├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .golangci.yaml ├── LICENSE ├── Makefile ├── README.md ├── cli ├── .gitignore ├── openspa-client │ └── main.go ├── openspa-server │ └── main.go └── openspa │ └── main.go ├── docs ├── .gitignore ├── assets │ └── openspa_brief.png ├── benchmarks │ ├── data_raw │ │ ├── test1 │ │ │ ├── README.md │ │ │ ├── test1_adk.csv │ │ │ ├── test1_no_adk.csv │ │ │ └── test1_xadk.csv │ │ ├── test2 │ │ │ ├── README.md │ │ │ ├── test2_adk.csv │ │ │ ├── test2_no_adk.csv │ │ │ └── test2_xadk.csv │ │ └── test3 │ │ │ ├── README.md │ │ │ ├── test3_adk.csv │ │ │ ├── test3_no_adk.csv │ │ │ └── test3_xadk.csv │ ├── dut_scripts │ │ ├── check_nic_irq_affinity.py │ │ ├── cpus_id │ │ ├── mmwatch │ │ ├── rtwatch │ │ └── setup_rxqueues.sh │ └── trex │ │ ├── ospa_benchmark.py │ │ ├── ospa_dos_for_benchmarks.py │ │ ├── ospa_legit_for_benchmarks.py │ │ └── ospa_req_legit.bin └── protocol.md ├── examples ├── Makefile └── authorization │ ├── authorization.py │ └── authorization_test.py ├── go.mod ├── go.sum ├── internal ├── adk.go ├── adk_test.go ├── authorization.go ├── authorization_test.go ├── client.go ├── client_mocks_and_stubs.go ├── client_test.go ├── cmd │ ├── adk.go │ ├── helpers.go │ ├── ip.go │ ├── req.go │ ├── server.go │ └── version.go ├── common.go ├── common_test.go ├── firewall.go ├── firewall_rule_manager.go ├── firewall_rule_manager_test.go ├── fw_command.go ├── fw_command_test.go ├── fw_iptables.go ├── fw_iptables_test.go ├── ip.go ├── mocks_and_stubs.go ├── observability.go ├── observability │ ├── metrics.go │ ├── metrics │ │ ├── prometheus.go │ │ └── prometheus_test.go │ ├── metrics_global.go │ ├── metrics_global_test.go │ ├── metrics_stub.go │ └── metrics_test.go ├── ospa.go ├── ospa_test.go ├── server.go ├── server_config.go ├── server_config_test.go ├── server_handler.go ├── server_handler_test.go ├── server_http.go ├── server_http_test.go ├── server_test.go ├── server_udp.go ├── server_udp_test.go ├── version.go └── xdp │ ├── Makefile │ ├── README.md │ ├── adk.go │ ├── adk_common.go │ ├── adk_common_test.go │ ├── adk_none.go │ ├── bpf_bpfeb.go │ ├── bpf_bpfeb.o │ ├── bpf_bpfel.go │ ├── bpf_bpfel.o │ ├── byteorder_eb.go │ ├── byteorder_el.go │ ├── go.mod │ ├── go.sum │ ├── headers │ ├── LICENSE.BSD-2-Clause │ ├── bpf_endian.h │ ├── bpf_helper_defs.h │ ├── bpf_helpers.h │ └── update.sh │ ├── openspa_adk.c │ ├── openspa_adk.h │ └── xdp.go └── pkg └── openspalib ├── adk.go ├── adk_test.go ├── common.go ├── common_test.go ├── container_helpers.go ├── container_helpers_test.go ├── crypto ├── aes_256_cbc.go ├── aes_256_cbc_test.go ├── cipher_rsa_sha256_aes_256_cbc.go ├── cipher_rsa_sha256_aes_256_cbc_test.go ├── cipher_suite.go ├── common.go ├── common_test.go ├── mocks_and_stubs.go ├── rsa.go ├── rsa_test.go └── tlv.go ├── entries.go ├── entries_test.go ├── header.go ├── header_test.go ├── openspalib_test.go ├── request.go ├── request_test.go ├── response.go ├── response_test.go ├── tlv ├── README.md ├── container.go ├── container_test.go ├── item.go ├── item_test.go ├── mocks_and_stubs.go └── tlv.go └── version.go /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | # This workflow was based on spf13/cobra 2 | # https://github.com/spf13/cobra/blob/22b617914c8890ba20db7ceafcdc2ef4ca4817d3/.github/workflows/test.yml 3 | 4 | name: CI 5 | 6 | on: 7 | push: 8 | pull_request: 9 | 10 | jobs: 11 | golangci-lint: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/setup-go@v3 15 | with: 16 | go-version: '1.19.0' 17 | 18 | - uses: actions/checkout@v3 19 | 20 | - uses: golangci/golangci-lint-action@v3.2.0 21 | with: 22 | version: latest 23 | args: --verbose 24 | 25 | test-unix: 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | platform: 30 | - ubuntu 31 | - macOS 32 | go: 33 | - 18 34 | - 19 35 | name: '${{ matrix.platform }} | 1.${{ matrix.go }}.x' 36 | runs-on: ${{ matrix.platform }}-latest 37 | steps: 38 | - uses: actions/setup-go@v3 39 | with: 40 | go-version: 1.${{ matrix.go }}.x 41 | 42 | - uses: actions/checkout@v3 43 | 44 | - uses: actions/cache@v3 45 | with: 46 | path: ~/go/pkg/mod 47 | key: ${{ runner.os }}-1.${{ matrix.go }}.x-${{ hashFiles('**/go.sum') }} 48 | restore-keys: ${{ runner.os }}-1.${{ matrix.go }}.x- 49 | 50 | - run: make bench 51 | - run: make test 52 | - run: make build 53 | 54 | # Inspired by: https://github.com/lluuiissoo/go-testcoverage 55 | - name: Quality Gate - Test coverage shall be above threshold 56 | env: 57 | TESTCOVERAGE_THRESHOLD: 70 58 | run: | 59 | echo "Quality Gate: checking test coverage is above threshold ..." 60 | echo "Threshold : $TESTCOVERAGE_THRESHOLD %" 61 | totalCoverage=`make coverage | tail -n 1 | grep total | grep -Eo '[0-9]+\.[0-9]+'` 62 | echo "Current test coverage : $totalCoverage %" 63 | if (( $(echo "$totalCoverage $TESTCOVERAGE_THRESHOLD" | awk '{print ($1 > $2)}') )); then 64 | echo "OK" 65 | else 66 | echo "Current test coverage is below threshold. Please add more unit tests or adjust threshold to a lower value." 67 | echo "Failed" 68 | exit 1 69 | fi 70 | 71 | test-win: 72 | name: MINGW64 73 | defaults: 74 | run: 75 | shell: msys2 {0} 76 | runs-on: windows-latest 77 | steps: 78 | - shell: bash 79 | run: git config --global core.autocrlf input 80 | 81 | - uses: msys2/setup-msys2@v2 82 | with: 83 | msystem: MINGW64 84 | update: true 85 | install: > 86 | git 87 | make 88 | unzip 89 | mingw-w64-x86_64-go 90 | python3 91 | 92 | - uses: actions/checkout@v3 93 | 94 | - uses: actions/cache@v3 95 | with: 96 | path: ~/go/pkg/mod 97 | key: ${{ runner.os }}-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} 98 | restore-keys: ${{ runner.os }}-${{ matrix.go }}- 99 | 100 | # Windows runners are too slow for running benchmarks, since it will kill the process for taking too long (11 min). 101 | # - run: make bench 102 | - run: make test 103 | - run: make build-windows_amd64 104 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # --- GO --- 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # CMake 16 | cmake-build-*/ 17 | 18 | 19 | # --- Python --- 20 | # Byte-compiled / optimized / DLL files 21 | __pycache__/ 22 | *.py[cod] 23 | *$py.class 24 | 25 | # Distribution / packaging 26 | .Python 27 | build/ 28 | develop-eggs/ 29 | dist/ 30 | downloads/ 31 | eggs/ 32 | .eggs/ 33 | lib/ 34 | lib64/ 35 | parts/ 36 | sdist/ 37 | var/ 38 | wheels/ 39 | *.egg-info/ 40 | .installed.cfg 41 | *.egg 42 | MANIFEST 43 | 44 | # PyInstaller 45 | # Usually these files are written by a python script from a template 46 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 47 | *.manifest 48 | *.spec 49 | 50 | # Installer logs 51 | pip-log.txt 52 | pip-delete-this-directory.txt 53 | 54 | # Unit test / coverage reports 55 | htmlcov/ 56 | .tox/ 57 | .coverage 58 | .coverage.* 59 | .cache 60 | nosetests.xml 61 | coverage.xml 62 | *.cover 63 | .hypothesis/ 64 | .pytest_cache/ 65 | 66 | # pyenv 67 | .python-version 68 | 69 | # Environments 70 | .env 71 | .venv 72 | env/ 73 | venv/ 74 | ENV/ 75 | env.bak/ 76 | venv.bak/ 77 | 78 | # --- Custom --- 79 | .idea/ 80 | .DS_Store 81 | temp/ 82 | artifacts/ -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 5m 3 | 4 | linters: 5 | disable-all: true 6 | enable: 7 | - bodyclose 8 | - depguard 9 | - dogsled 10 | - dupl 11 | - errcheck 12 | - exportloopref 13 | - exhaustive 14 | #- funlen 15 | - gas 16 | - gochecknoinits 17 | - goconst 18 | - gocritic 19 | - gocyclo 20 | - gofmt 21 | - goimports 22 | # - gomnd 23 | - goprintffuncname 24 | - gosec 25 | - gosimple 26 | - govet 27 | - ineffassign 28 | - lll 29 | - megacheck 30 | - misspell 31 | - nakedret 32 | # - noctx 33 | - nolintlint 34 | - revive 35 | - rowserrcheck 36 | - staticcheck 37 | - stylecheck 38 | - typecheck 39 | - unconvert 40 | - unparam 41 | - unused 42 | - whitespace 43 | fast: false 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Gregor R. Krmelj 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 4 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 5 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 6 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 9 | Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 12 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 13 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 14 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIR ?= ./artifacts 2 | 3 | 4 | .PHONY: build 5 | build: 6 | $(shell mkdir -p $(BUILD_DIR)) 7 | $(MAKE) build-linux_amd64 8 | $(MAKE) build-darwin_amd64 9 | $(MAKE) build-windows_amd64 10 | $(MAKE) build-server-xdp-linux_amd64 11 | 12 | .PHONY: build-server-xdp-linux_amd64 13 | build-server-xdp-linux_amd64: 14 | GOOS=linux GOARCH=amd64 go build -tags xdp -o $(BUILD_DIR)/openspa_xdp_linux_amd64 ./cli/openspa 15 | 16 | .PHONY: build-linux_amd64 17 | build-linux_amd64: 18 | GOOS=linux GOARCH=amd64 go build -o $(BUILD_DIR)/openspa_linux_amd64 ./cli/openspa 19 | 20 | .PHONY: build-darwin_amd64 21 | build-darwin_amd64: 22 | GOOS=darwin GOARCH=amd64 go build -o $(BUILD_DIR)/openspa_darwin_amd64 ./cli/openspa 23 | 24 | .PHONY: build-windows_amd64 25 | build-windows_amd64: 26 | GOOS=windows GOARCH=amd64 go build -o $(BUILD_DIR)/openspa_windows_amd64 ./cli/openspa 27 | 28 | .PHONY: test 29 | test: 30 | go test ./... 31 | cd ./examples && $(MAKE) test 32 | 33 | .PHONY: bench 34 | bench: 35 | go test -bench=. ./... 36 | 37 | .PHONY: lint 38 | lint: 39 | golangci-lint run 40 | 41 | .PHONY: clean 42 | clean: 43 | $(RM) -drf "$(BUILD_DIR)" 44 | 45 | .PHONY: coverage 46 | coverage: 47 | $(shell mkdir -p $(BUILD_DIR)) 48 | go test $(shell go list ./... | grep -v internal/xdp) -covermode=count -coverprofile=$(BUILD_DIR)/coverage_raw.out 49 | cat $(BUILD_DIR)/coverage_raw.out | grep -v "mock" | grep -v "stub" > $(BUILD_DIR)/coverage_filtered.out 50 | go tool cover -func=$(BUILD_DIR)/coverage_filtered.out 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenSPA 2 | 3 | [![CI](https://github.com/greenstatic/openspa/actions/workflows/ci.yaml/badge.svg)](https://github.com/greenstatic/openspa/actions/workflows/ci.yaml) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/greenstatic/openspa.svg)](https://pkg.go.dev/github.com/greenstatic/openspa) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/greenstatic/openspa)](https://goreportcard.com/report/github.com/greenstatic/openspa) 6 | ![License](https://img.shields.io/github/license/greenstatic/openspa) 7 | 8 | OpenSPA: An open and extensible Single Packet Authorization (SPA) implementation of the [OpenSPA Protocol](docs/protocol.md). 9 | 10 | [v1](https://github.com/greenstatic/openspa/tree/v1) of the protocol was created in 2018 and while functioning, it has a 11 | few shortcomings which are being resolved in v2 (currently the dev branch) of the protocol. 12 | 13 | **v2 is currently as of 2022 under heavy development.** No guarantees are made that it will remain backwards compatible 14 | in it's current form. 15 | We WILL break it during development. 16 | 17 | v1 was never production ready and so any PR regarding v1 will be rejected. 18 | 19 | ## What is OpenSPA? 20 | OpenSPA is an open and extensible SPA implementation built upon the OpenSPA Protocol. 21 | OpenSPA allows the deployment of a service on an internal network or the internet, that is hidden to all unauthorized 22 | users. 23 | Authorized users authenticate by sending a single packet to the OpenSPA server, which will reveal itself only if the 24 | user is authorized to access the service. 25 | 26 | OpenSPA builds what essentially is a dynamic firewall. 27 | 28 | ![OpenSPA-Demo](docs/assets/openspa_brief.png) 29 | 30 | Unauthorized users will not be able to detect via the network the presence of the hidden service (no ping, traceroute, 31 | port scans, fingerprinting, etc.). 32 | Once the user sends an OpenSPA request packet (via UDP) and they are authorized only then will the server respond with 33 | a response. 34 | Unauthorized users thus will also be unable to confirm the existence of the OpenSPA service. 35 | 36 | ## Version 1 vs. 2? 37 | The major difference between v1 and v2 of the OpenSPA protocol is how binary messages (request & response) are encoded. 38 | Version 1 had a well-defined binary format (e.g. offset X with a length of 32 bits contains the client's IP address). 39 | While this of course worked, it also proved very difficult to extend and modify. 40 | Which is why version 2 uses TLVs to encode the binary messages. 41 | This allows v2 to be customized and extended very easily for different use-cases. 42 | 43 | Version 2 also brings native support for IPtables, making extension scripts optional (or rather an alternative to the 44 | native IPtables integration to support different firewalls). 45 | 46 | ## Version 2 Status 47 | Completed: 48 | * openspalib (`pkg/openspalib`) - library for the OpenSPA protocol. With this you can implement your own OpenSPA client 49 | and server 50 | * Client (`cli/openspa-client`) - OpenSPA client CLI 51 | * Server (`cli/openspa-server`) - OpenSPA server CLI 52 | * Config file support 53 | * Native IPtables integration 54 | * External firewall integration 55 | * External authorization integration 56 | * adk (Anti DoS Knocking protection) implemented using TOTP 57 | * Server should expose Prometheus metrics via HTTP 58 | * eBPF/XDP adk acceleration (Anti DoS knocking protection) 59 | * Benchmarks (ADK with XDP and without) 60 | 61 | Planned: 62 | * ECC support 63 | * x509 certificate support 64 | * Helper utility to generate keys 65 | * Server external authentication support 66 | * Replay attack prevention 67 | * Use `SO_REUSEPORT` to increase performance on multi-core, multi-NIC queue systems [good blog post about the issue](https://blog.cloudflare.com/how-to-receive-a-million-packets/) 68 | 69 | ## Building from Source 70 | ```sh 71 | $ sudo apt install build-essential make git 72 | $ git clone https://github.com/greenstatic/openspa.git 73 | $ cd openspa 74 | $ make build 75 | # Build artifacts in the: ./artifacts directory 76 | ``` 77 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | openspa/openspa 2 | openspa-client/openspa-client 3 | openspa-server/openspa-server -------------------------------------------------------------------------------- /cli/openspa-client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/greenstatic/openspa/internal/cmd" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func main() { 12 | rootCmdSetup(rootCmd) 13 | if err := rootCmd.Execute(); err != nil { 14 | fmt.Println(err) 15 | os.Exit(1) 16 | } 17 | } 18 | 19 | var rootCmd = &cobra.Command{ 20 | Use: "openspa-client", 21 | Short: "Send OpenSPA request packets to get access to hidden services", 22 | Run: func(cmd *cobra.Command, args []string) { 23 | _ = cmd.Help() 24 | }, 25 | PreRun: cmd.PreRunLogSetupFn, 26 | } 27 | 28 | func rootCmdSetup(c *cobra.Command) { 29 | cmd.RootCmdSetupFlags(c) 30 | 31 | c.AddCommand(cmd.IPCmd) 32 | cmd.IPCmdSetup(cmd.IPCmd) 33 | 34 | c.AddCommand(cmd.ReqCmd) 35 | cmd.ReqCmdSetup(cmd.ReqCmd) 36 | 37 | c.AddCommand(cmd.VersionCmdGet(false)) 38 | } 39 | -------------------------------------------------------------------------------- /cli/openspa-server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/greenstatic/openspa/internal/cmd" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func main() { 12 | rootCmdSetup(cmd.ServerCmd) 13 | if err := cmd.ServerCmd.Execute(); err != nil { 14 | fmt.Println(err) 15 | os.Exit(1) 16 | } 17 | } 18 | 19 | func rootCmdSetup(c *cobra.Command) { 20 | cmd.RootCmdSetupFlags(c) 21 | cmd.ServerCmdSetup(c) 22 | 23 | c.AddCommand(cmd.ADKCmd) 24 | cmd.ADKCmdSetup(cmd.ADKCmd) 25 | c.AddCommand(cmd.VersionCmdGet(true)) 26 | } 27 | -------------------------------------------------------------------------------- /cli/openspa/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/greenstatic/openspa/internal/cmd" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func main() { 12 | rootCmdSetup(rootCmd) 13 | if err := rootCmd.Execute(); err != nil { 14 | fmt.Println(err) 15 | os.Exit(1) 16 | } 17 | } 18 | 19 | var rootCmd = &cobra.Command{ 20 | Use: "openspa", 21 | Run: func(cmd *cobra.Command, args []string) { 22 | _ = cmd.Help() 23 | }, 24 | PreRun: cmd.PreRunLogSetupFn, 25 | } 26 | 27 | func rootCmdSetup(c *cobra.Command) { 28 | cmd.RootCmdSetupFlags(c) 29 | 30 | c.AddCommand(clientCmd) 31 | clientCmdSetup(clientCmd) 32 | 33 | c.AddCommand(cmd.ServerCmd) 34 | cmd.ServerCmdSetup(cmd.ServerCmd) 35 | 36 | c.AddCommand(cmd.ADKCmd) 37 | cmd.ADKCmdSetup(cmd.ADKCmd) 38 | 39 | c.AddCommand(cmd.VersionCmdGet(true)) 40 | } 41 | 42 | var clientCmd = &cobra.Command{ 43 | Use: "client", 44 | Short: "Send OpenSPA request packets to get access to hidden services", 45 | Run: func(cmd *cobra.Command, args []string) { 46 | _ = cmd.Help() 47 | }, 48 | PreRun: func(c *cobra.Command, args []string) { 49 | cmd.PreRunLogSetupFn(c, args) 50 | }, 51 | } 52 | 53 | func clientCmdSetup(c *cobra.Command) { 54 | c.AddCommand(cmd.ReqCmd) 55 | cmd.ReqCmdSetup(cmd.ReqCmd) 56 | 57 | c.AddCommand(cmd.IPCmd) 58 | cmd.IPCmdSetup(cmd.IPCmd) 59 | } 60 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | measurements -------------------------------------------------------------------------------- /docs/assets/openspa_brief.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenstatic/openspa/edc748cfbcd34acb2865e24fdd1430c941bed0e5/docs/assets/openspa_brief.png -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test1/README.md: -------------------------------------------------------------------------------- 1 | # Test 1 2 | Question we wished to answer: What is the maximum DoS attack the server can defend against using different number of CPU cores? 3 | 4 | Data was repeated for each setup (no protection, ADK feature and XADK feature). 5 | 6 | CSV files contain in the first line the number of CPU cores. 7 | And 180 measurements of the number of requests processed for each core. 8 | -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test2/README.md: -------------------------------------------------------------------------------- 1 | # Test 2 2 | Question we wished to answer: CPU usage during DoS attack. 3 | 4 | We used the `mmwatch` script to scrape the `/metrics` endpoint of the server. 5 | 6 samples were taken for each DoS attack size, each sample representing the average free CPU in the last 30 seconds. 6 | To get the utilization subtract by `100` (inverse). 7 | 8 | CSV files contain in the first line the size of the attack. 9 | -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test2/test2_adk.csv: -------------------------------------------------------------------------------- 1 | 10000,25000,50000,100000,150000,200000,250000,300000,350000,400000 2 | 79.75,62.65,57.62,47.47,44.75,14.11,5.21,0,0,0 3 | 85.79,65.82,57.04,50.56,34.88,14.83,6.56,0,0,0 4 | 83.57,71.61,53.09,65.85,24.76,17.16,5.15,0,0,0 5 | 71.69,44.53,71.07,37.53,30.53,23.1,5.12,0,0,0 6 | 60.79,72.91,58.26,48.38,43.4,14.33,5.16,0,0,0 7 | 73.48,74.05,56.31,49.17,35.96,14.9,6.68,0,0,0 -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test2/test2_no_adk.csv: -------------------------------------------------------------------------------- 1 | 100,200,300,400,500,600,700,800,900,1000 2 | 67.21,46.01,42.93,47.63,59.09,33.95,23.01,14.62,6.3,0 3 | 66.65,45.06,42.26,48.06,54.42,33.52,23.6,14.72,6.22,0 4 | 69.42,45.3,41.93,48.57,51.82,33.89,23.45,14.89,6.38,0 5 | 72,44.88,42.14,47.94,36.75,34.93,24.53,14.98,6.55,0 6 | 73.15,45.37,45.14,47.48,32.91,34.39,24.61,14.54,6.55,0 7 | 75.37,48.04,42.86,48.13,30.85,33.99,24.46,14.57,6.37,0 -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test2/test2_xadk.csv: -------------------------------------------------------------------------------- 1 | 50000,100000,250000,500000,1000000,1500000,2000000,2500000,3000000,3500000,4000000,4500000,5000000,5500000,6000000,6500000,7000000 2 | 99.78,93.84,87.24,84.34,72.39,56.01,76.95,57.31,50.89,47.22,42.29,39.05,36.12,27.84,26.67,4.88,0 3 | 94.37,94.83,90.73,77.04,58.65,63.24,51.09,49.14,46.18,51.57,46.14,37.24,34.91,29.81,26.75,6.08,0 4 | 95.32,92.74,95.09,85.92,87.99,68.59,54.94,63.14,68.71,46.46,46.96,39.76,35.62,23.66,26.45,0.1,0 5 | 94.78,96.12,97.88,95.15,91.81,73.77,65.33,53.96,48.94,40.77,46.59,40.5,30.02,30.27,27.5,4.17,0 6 | 97,98.31,89.8,92.33,87.96,65.07,49.32,51.36,49.99,46.35,48.9,40.27,28.44,27.26,27.21,1.36,0 7 | 96.72,92.45,87.38,87.71,71.57,59.3,70.96,52.56,56.29,43.8,43.3,40.45,27.84,31.16,26.11,0.1,0 -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test3/README.md: -------------------------------------------------------------------------------- 1 | # Test 3 2 | Question we wished to answer: How does a DoS attack affect legitimate traffic? 3 | 4 | We were sending 30 requests/sec as well as simulating a DoS attack. 5 | 6 | CSV files contain in the first line the size of the attack, while the results contain the number of sent responses. 7 | Each DoS attack size we tested for a duration of 60 seconds. 8 | -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test3/test3_adk.csv: -------------------------------------------------------------------------------- 1 | 0,100000,200000,300000,400000,500000,600000,700000,800000,900000,1000000,1100000,1200000,1300000 2 | 30.72,29.82,26.41,10.31,14.88,16.8,14.5,9.32,10.57,9.49,8.7,7.39,6.67,3.74 3 | 30.21,30.41,26.2,9.66,17.34,13.9,13.19,8.66,11.23,7.74,7.82,7.66,8.29,3.36 4 | 30.61,29.07,25.48,9.28,15.11,14.38,13.53,9.78,9.08,8.33,6.88,7.29,7.14,4.18 5 | 30.3,28.89,25.74,10.58,15.55,15.19,14.26,8.85,9.54,6.66,7.94,8.15,9.02,4.56 6 | 30.99,28.95,25.25,13.79,16.78,14.03,13.57,11.92,10.22,7.79,7.93,9.02,7.97,4.26 7 | 30.35,28.33,24.5,15.31,16.31,13.01,14.7,10.91,8.61,9.34,7.93,10.51,7.49,4.13 8 | 30.52,30.01,26.75,17.06,14.59,15.91,15.35,11.89,8.26,7.17,5.96,10.21,7.21,3.55 9 | 30.26,29.5,26.25,16.53,14.3,15.88,16.09,11.45,8.59,7.55,7.93,9.06,9.05,4.25 10 | 30.48,29.6,27.48,21.63,12.1,16.85,17.45,13.64,8.29,7.73,9.47,7.03,8.02,7.13 11 | 30.24,29.8,26.24,21.21,16.55,16.92,15.73,11.77,9.59,7.87,8.2,6.98,8.47,7.03 12 | 30.96,30.25,27.48,21.11,13.72,16.38,13.8,11.39,9.75,7.89,8.06,7.49,7.73,7.47 13 | 30.33,29.98,26.11,21.44,14.36,16.61,14.82,11.63,9.33,6.92,9.03,7.71,8.32,7.24 14 | 30.67,29.99,26.06,23.22,13.68,14.8,16.41,12.75,10.16,6.46,10.46,8.31,8.62,5.1 15 | 30.18,29.35,26.89,23.99,15.26,12.35,15.63,11.87,11.02,5.7,10.18,8.65,7.31,5.03 16 | 30.93,30.02,26.32,26.85,13.13,13.6,15.24,10.89,11.45,7.8,10.09,8.29,7.61,4.01 17 | 30.97,30.01,26.66,23.92,13,13.8,12.62,7.92,12.23,7.4,10.49,7.61,7.27,3.99 18 | 30.83,28.37,25.71,23.35,13.5,13.83,12.75,8.46,9.58,7.66,7.25,9.3,10.14,5.95 19 | 30.27,28.05,25.23,25.67,16.16,14.84,10.33,9.23,7.76,7.79,8.57,8.61,8.53,4.48 20 | 30.63,28.52,25.61,23.23,15.58,13.85,11.17,11.05,7.88,7.4,6.27,6.29,7.73,5.21 21 | 30.17,29.11,26.67,23,15.71,12.93,11.52,10.97,6.42,6.67,6.63,5.64,7.37,5.08 22 | 30.93,29.9,26.21,21.5,16.35,13.39,10.71,10.44,6.21,6.33,6.78,6.78,8.63,5.54 23 | 30.46,30.45,26.6,22.63,15.68,17.09,14.36,8.72,8.55,5.64,7.85,7.35,8.77,6.24 24 | 31.07,28.1,26.67,23.69,14.27,16.55,13.12,7.33,6.75,5.79,5.42,8.68,6.39,5.1 25 | 30.39,27.91,26.21,24.85,16.54,15.7,13.06,8.16,6.37,7.4,6.18,10.28,7.65,4.05 26 | 30.69,28.45,25.1,22.82,13.27,14.29,15.94,8.54,7.64,7.16,5.56,9.1,5.31,5.49 27 | 30.69,28.09,26.41,24.78,14.64,12.14,15.89,7.73,8.77,7.08,7.28,9.55,5.65,4.74 28 | 30.69,28.54,25.71,25.89,15.73,12.07,16.36,9.37,8.39,7.5,8.1,9.73,6.33,4.85 29 | 30.35,28.63,26.71,20.86,18.37,13.88,13.68,8.64,6.67,5.73,8.5,8.82,7.12,4.87 30 | 30.52,27.19,24.74,20.83,15.12,14.94,14.76,7.29,8.83,4.87,7.25,8.41,5.54,4.94 31 | 30.11,26.46,25.37,24.41,16.06,15.89,14.81,7.65,7.88,2.93,6.6,9.16,7.27,5.97 32 | 31.06,28.73,25.56,21.12,14.47,14.87,14.9,6.79,9.88,3.44,5.28,7.55,7.1,6.45 33 | 30.38,29.71,27.28,22.94,16.73,13.44,14.88,7.85,8.94,3.22,6.64,8.77,8.01,5.22 34 | 30.54,29.71,25.64,21.47,13.81,13.15,15.36,7.93,9.42,3.1,6.78,7.85,6,5.09 35 | 30.27,29.85,25.07,24.1,16.41,13.51,16.18,5.94,9.17,3.53,7.85,7.93,7.46,4.52 36 | 30.48,29.28,27.54,23.93,16.7,13.25,14.03,7.92,7.08,4.76,5.92,8.46,6.7,5.26 37 | 30.59,29,26.14,25.97,17.26,14.05,13.45,7.46,7.5,3.87,5.46,6.21,7.85,5.6 38 | 30.79,29.5,26.44,23.87,17.05,12.03,12.73,9.18,7.22,3.91,5.7,7.56,4.91,6.8 39 | 30.25,30.59,26.22,22.83,14.52,14.43,11.81,10.03,6.11,2.96,3.84,8.73,5.46,5.38 40 | 30.47,27.67,25.98,22.91,12.26,12.66,13.9,7.52,7.51,3.95,4.42,10.37,6.23,6.65 41 | 30.24,28.84,25.86,23.34,14.55,13.83,13.39,9.7,4.74,3.98,3.2,8.65,5.59,4.83 42 | 30.96,26.79,25.93,25.04,15.77,14.84,13.13,7.82,4.37,3.97,1.6,7.79,5.29,5.38 43 | 30.33,26.4,25.84,25.02,12.84,13.36,13.06,10.41,3.67,3.48,1.3,8.39,5.59,6.65 44 | 30.67,27.55,26.78,19.93,15.42,13.18,12.47,7.68,4.81,2.24,2.14,8.16,6.79,5.31 45 | 30.18,28.13,25.89,21.85,14.14,13.52,12.18,8.79,3.4,2.11,4.53,7.05,6.9,3.65 46 | 30.44,28.07,26.81,23.42,13.07,11.71,13.59,8.4,2.69,3.05,4.27,7.52,6.91,3.33 47 | 30.22,28.39,27.76,21.12,10.99,13.86,13.72,8.65,3.82,4.5,4.61,8.22,7.42,3.64 48 | 31.11,27.69,27.38,22.94,12.5,12.87,14.78,8.78,3.41,4.23,4.28,9.06,7.71,5.29 49 | 30.41,29.69,27.06,24.47,16.25,11.38,13.39,5.89,2.21,3.61,4.14,7.53,7.32,4.62 50 | 31.04,29.7,27.03,19.66,14.06,13.61,11.15,6.91,3.08,4.28,2.07,8.72,7.16,5.28 51 | 30.52,30.19,25.4,20.23,12.97,12.25,10.53,5.93,2.04,3.14,3.51,6.34,9.03,5.64 52 | 30.61,29.45,25.07,24.11,11.99,11.13,10.26,5.46,3.02,2.56,2.76,6.67,5.5,6.78 53 | 30.65,29.73,26.04,24.93,8.99,12,10.08,3.72,2.51,2.27,2.86,4.32,5.72,5.89 54 | 31.33,28.72,25.89,23.96,9.94,9.96,10.54,4.86,2.24,3.14,2.92,4.14,5.86,5.45 55 | 30.51,27.73,26.44,22.87,11.47,9.98,10.22,4.91,1.62,2.06,1.96,3.57,6.4,5.2 56 | 30.6,27.86,25.6,23.44,12.17,7.47,9.07,5.45,2.31,3.03,0.98,3.77,6.7,5.07 57 | 30.3,29.28,25.18,22.61,12.59,10.17,11.04,3.22,2.64,3,1.97,4.85,6.29,7.04 58 | 30.99,27.14,25.59,24.67,11.24,7.58,11.46,3.1,2.8,2.49,1.99,5.43,4.65,6.02 59 | 30.35,28.42,26.16,26.34,11.12,9.24,10.68,4.05,2.4,2.25,1.98,4.2,4.8,4.99 60 | 30.67,28.57,26.94,27.52,11.5,11.62,10.84,3.52,3.68,3.1,1.49,4.57,6.4,6.49 61 | 30.19,29.78,26.47,28.12,10.75,12.74,10.37,3.23,2.83,2.54,2.74,4.79,7.7,4.24 -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test3/test3_no_adk.csv: -------------------------------------------------------------------------------- 1 | 0,500,1000,2000,3000,4000,5000,6000,7000,8000,9000,10000 2 | 30.96,30.38,28.24,14.73,9.24,7.69,6.62,5.64,5.2,3.51,3.98,3.65 3 | 30.48,31.03,27.49,13.87,9.62,8.3,6.28,6.28,4.09,4.23,3.99,2.82 4 | 30.59,30.52,28.24,14.36,9.76,7.65,6.14,5.12,4.04,3.62,3.98,3.39 5 | 30.15,30.6,27.49,14.11,9.38,7.79,6.04,5.56,4,4.28,3.97,3.18 6 | 30.57,30.3,28.1,14.56,10.14,7.39,5.99,5.26,4.48,3.63,3.48,4.07 7 | 30.63,30.99,27.05,15.2,10.51,7.16,5,6.63,3.74,4.31,3.72,3.52 8 | 30.66,30.5,27.39,15.52,10.26,7.54,6.95,4.81,5.33,3.64,3.84,3.76 9 | 30.33,30.59,27.55,15.26,10.57,7.27,5.95,5.87,4.65,3.8,3.92,2.87 10 | 30.51,30.15,27.28,15.06,10.24,7.6,6.48,5.41,4.82,3.9,3.45,2.93 11 | 30.11,30.57,28,13.96,10.12,7.26,5.71,6.21,4.89,4.43,2.71,2.95 12 | 31.05,30.63,27.86,14.48,10.01,7.13,6.82,5.1,4.42,3.7,3.36,3.46 13 | 30.38,30.66,27.93,14.17,10.45,7.53,6.41,5.52,4.21,4.35,4.15,3.23 14 | 30.54,30.33,28.32,14.09,9.73,8.22,6.67,4.26,5.08,3.66,3.56,3.59 15 | 30.27,30.67,27.66,12.98,10.31,8.11,5.81,6.09,4.02,4.8,3.78,2.8 16 | 30.48,30.04,27.69,14.99,9.65,7.52,6.41,5.05,4.01,4.4,3.87,4.37 17 | 30.24,31.02,27.71,14.43,10.27,7.72,5.68,5.49,4.48,4.18,3.92,3.67 18 | 30.62,30.36,27.35,14.64,10.14,7.86,6.84,4.25,5.21,4.07,3.46,3.33 19 | 30.66,30.53,27.54,14.32,10.51,7.4,6.88,6.08,3.61,4.54,3.71,3.15 20 | 30.67,30.26,27.27,14.59,10.21,8.15,6.41,5.04,4.28,4.25,2.35,3.56 21 | 30.19,30.48,27.99,14.22,10.6,7.58,6.18,6.48,4.61,4.12,2.67,3.28 22 | 31.09,30.59,27.36,14.61,9.76,7.75,7.59,4.74,5.31,3.55,3.32,3.12 23 | 30.89,30.79,28.18,14.24,10.32,7.37,5.77,5.84,4.63,4.25,4.16,2.55 24 | 30.79,30.25,27.46,14.05,9.66,6.66,5.86,5.42,5.29,3.12,3.56,3.28 25 | 30.4,31.12,27.59,14.02,9.78,7.29,5.93,5.18,4.64,4.04,3.76,3.62 26 | 30.54,30.41,28.79,14.93,9.35,8.14,6.43,4.59,4.8,4.49,4.38,4.28 27 | 30.12,31.05,26.77,14.97,10.17,7.04,6.21,5.76,4.87,3.73,3.68,3.64 28 | 31.06,31.02,27.25,14.91,9.54,8.52,6.57,3.38,4.44,3.87,2.83,3.31 29 | 30.88,30.86,28.12,14.39,10.27,7.73,6.29,6.15,3.7,3.42,3.91,3.14 30 | 30.79,30.43,27.43,14.19,10.09,8.32,6.61,5.07,4.33,4.68,3.44,3.07 31 | 30.39,30.56,26.59,14.03,11.04,7.16,7.26,5.01,4.16,4.34,4.72,2.52 32 | 30.54,30.13,27.65,14.44,9.98,7.54,7.13,6.01,5.05,3.16,4.34,3.74 33 | 30.12,31.07,27.32,13.22,10.93,7.24,5.55,6.5,4.51,4.58,4.15,3.87 34 | 31.06,30.38,27.52,14.53,9.46,7.12,6.27,5.23,4.25,4.77,3.58,3.42 35 | 30.38,30.54,28.26,15.27,11.17,7.02,4.62,6.58,4.6,4.36,4.26,2.2 36 | 30.69,30.27,27.5,15.06,9.54,7.97,5.31,5.29,4.28,4.18,3.62,2.1 37 | 30.2,30.63,28.11,14.46,10.27,7.48,6.12,5.64,4.64,3.58,4.28,1.55 38 | 30.6,30.51,28.55,15.23,8.6,7.7,6.03,4.31,4.8,4.26,3.13,2.26 39 | 30.65,30.76,27.64,14.05,9.75,7.32,5.52,5.62,3.88,3.63,4.07,2.63 40 | 30.67,30.23,27.19,15.94,9.37,7.66,5.73,5.31,4.44,4.29,3.02,2.8 41 | 30.19,30.46,28.09,14.97,10.63,7.29,6.82,5.65,4.2,3.65,3.49,3.88 42 | 30.59,30.23,27.91,14.41,9.77,7.11,5.41,4.81,5.07,3.8,2.75,2.94 43 | 30.15,30.96,28.31,14.63,9.88,7.06,7.16,5.4,4.54,3.88,3.35,3.94 44 | 30.92,30.33,27.66,14.74,9.89,7.49,6.55,5.67,4.74,4.44,3.16,3.46 45 | 30.46,30.66,27.69,13.87,9.9,7.7,6.78,6.34,4.35,4.22,3.58,3.23 46 | 30.58,30.18,27.71,14.86,9.95,7.85,6.85,5.15,4.18,4.07,3.28,4.09 47 | 30.14,30.44,28.35,13.86,9.92,7.39,6.4,6.57,3.57,4.04,3.64,3.53 48 | 30.57,30.72,27.54,14.43,9.91,7.66,5.7,5.76,4.76,4.49,3.3,3.26 49 | 30.63,30.71,28.13,14.15,10.96,6.83,7.3,5.88,3.88,3.73,3.63,3.61 50 | 30.66,30.2,27.06,14.07,9.93,8.36,5.14,5.42,4.41,3.87,3.82,3.81 51 | 30.33,30.6,27.39,13.97,10.41,7.15,6.07,4.71,4.68,3.91,3.89,3.39 52 | 31.01,30.65,27.06,14.9,9.71,7.58,6.99,4.33,3.84,4.43,3.92,2.68 53 | 30.36,31.17,27.53,14.95,10.3,7.75,6.96,5.17,5.39,3.72,3.96,3.34 54 | 31.18,30.58,28.12,15.4,10.1,7.83,6.48,5.58,5.17,3.84,2.97,3.16 55 | 30.44,31.13,27.43,14.13,10.05,7.92,6.71,5.29,4.08,3.9,3.99,3.58 56 | 30.72,30.91,27.71,15.57,9.48,7.42,5.85,5.1,5.51,3.95,3.48,3.27 57 | 30.21,30.96,27.72,14.22,10.19,7.18,5.9,5.05,4.73,3.46,3.72,3.62 58 | 30.45,30.48,27.23,14.04,10.09,8.09,6.45,4.52,4.87,4.21,3.34,3.81 59 | 30.73,31.08,28.11,14.02,10,8,6.69,4.74,4.41,4.6,4.64,3.88 60 | 30.56,30.39,27.42,14.93,9.45,8.95,5.32,5.34,4.68,4.78,4.82,3.92 61 | 30.28,30.7,27.71,13.9,9.73,7.98,6.13,4.67,4.84,3.89,4.39,3.46 -------------------------------------------------------------------------------- /docs/benchmarks/data_raw/test3/test3_xadk.csv: -------------------------------------------------------------------------------- 1 | 0,2000000,4000000,6000000,8000000,10000000,12000000,14000000,16000000,18000000,20000000 2 | 31.2,31,30.21,30.43,24.12,20.26,18.34,15.63,3.75,21.07,11.71 3 | 30.45,30.5,30.45,30.71,22.45,21.51,18.67,15.24,2.86,15.53,12.29 4 | 30.72,30.6,30.22,30.86,21.73,20.66,19.24,15.05,9.43,12.22,11.64 5 | 30.21,30.3,31.11,30.28,22.86,21.33,21,14.02,12.14,15.61,11.27 6 | 30.61,30.99,30.41,30.64,23.31,24.16,24,13.94,13,14.24,9.59 7 | 30.15,30.5,30.7,29.68,23.54,25.45,25.86,11.47,13.5,14.05,7.8 8 | 30.58,30.75,30.2,30.34,23.77,25.6,25.31,6.73,13.68,13.46,7.86 9 | 30.64,30.23,30.6,30.02,22.38,24.3,24.15,3.86,13.77,13.73,7.93 10 | 30.82,30.61,30.15,31.01,24.56,24.15,23.96,2.93,14.89,12.81,7.93 11 | 30.41,30.16,30.58,30.36,25.28,24.95,23.37,1.96,16.85,12.34,11.39 12 | 30.55,30.58,31.13,30.68,24.52,23.97,19.68,1.98,15.42,10.17,10.19 13 | 30.13,30.64,30.56,30.19,27.26,19.41,17.76,1.48,14.15,9.05,12.03 14 | 30.56,30.82,30.78,30.44,27.49,13.71,18.88,8.66,12.07,11.95,10.51 15 | 30.13,30.26,30.74,30.22,27.25,23.35,20.83,9.83,14.45,9.47,6.25 16 | 31.07,31.13,30.22,31.45,26.49,21.08,23.91,11.85,12.67,8.7,4.11 17 | 30.38,30.57,30.61,30.58,23.75,21.43,25.82,10.92,11.34,8.85,3.05 18 | 30.69,30.63,30.31,30.79,22.37,21.72,23.8,9.42,12.1,10.36,2.02 19 | 30.2,30.66,30.99,30.25,22.08,23.36,23.4,12.14,13.55,12.18,2 20 | 30.6,30.83,30.5,30.47,21.54,24.06,20.61,13,12.22,10.55,1.5 21 | 30.3,30.27,30.6,30.73,22.16,25.03,21.31,14,14.11,13.19,1.74 22 | 30.5,30.63,29.8,30.71,23.58,21.42,19.07,15.42,13.49,13.1,1.37 23 | 30.59,30.17,30.9,30.7,25.65,22.21,19.93,14.14,13.18,11.5,1.68 24 | 30.8,30.58,30.3,30.35,25.83,20.51,18.88,17.07,11.09,11.69,1.34 25 | 30.4,30.29,30.5,30.52,25.29,19.76,19.44,14.48,11.49,12.35,1.66 26 | 30.55,30.49,30.25,30.76,23.64,18.29,20.12,14.17,14.16,12.11,1.33 27 | 30.12,30.59,31.12,30.73,25.82,17.65,24.41,15.58,15.08,10.02,1.65 28 | 30.56,30.8,30.41,30.22,26.77,17.32,25.71,16.21,13.48,10.01,1.32 29 | 30.78,30.4,30.71,30.61,27.25,21.04,19.78,16.02,14.66,10.94,1.16 30 | 30.89,30.55,30.2,30.16,28.12,20.02,26.72,16.51,13.83,12.4,1.08 31 | 30.79,30.27,30.6,31.08,28.06,21.4,23.86,16.67,15.33,12.2,1.53 32 | 30.74,30.98,30.15,30.89,28.88,24.2,19.36,15.76,12.62,11.55,1.26 33 | 30.37,29.99,30.58,30.79,26.44,26.46,17.6,14.88,11.81,13.2,4.59 34 | 30.53,30.84,30.63,30.39,25.6,25.73,16.3,15.86,11.35,11.6,7.25 35 | 30.27,30.42,30.32,30.54,24.8,25.74,17.56,16.34,12.11,12.24,7.12 36 | 30.48,30.56,30.66,30.62,24.28,25.87,18.18,15.17,12.56,11.56,9.01 37 | 30.74,30.28,30.68,30.81,23.64,23.43,22.09,16.99,13.21,12.78,10 38 | 30.72,30.64,30.34,30.26,23.21,20.13,23.42,15.92,14.1,11.84,9.46 39 | 30.36,30.17,30.52,30.63,23.1,19.47,23.1,15.39,11.51,10.92,9.18 40 | 30.53,30.59,30.26,30.17,22.05,18.74,24.55,14.62,14.17,12.39,8.55 41 | 30.26,30.64,30.97,30.43,23.4,19.37,23.66,13.81,15.58,12.14,11.28 42 | 30.63,30.82,30.99,30.56,25.07,19.09,22.72,15.32,12.74,9.07,10.09 43 | 30.17,30.26,30.84,30.78,25.03,21.54,20.86,15.58,14.87,10.97,10.05 44 | 30.93,30.63,30.92,30.24,25.52,22.65,17.86,14.79,15.36,10.98,10.96 45 | 30.96,30.17,30.96,30.47,25.13,24.83,16.85,15.81,16.59,10.44,9.94 46 | 30.98,31.08,30.33,30.73,26.57,25.78,20.42,14.34,13.79,12.15,8.47 47 | 30.34,30.89,30.51,31.21,27.64,25.89,22.59,16.17,15.31,14.08,10.17 48 | 30.52,30.94,30.76,30.46,27.32,24.33,23.18,10.07,14.09,15.95,7.09 49 | 30.26,31.31,30.88,30.73,25.05,23.17,24.59,17.41,13.05,12.43,7.5 50 | 30.63,30.66,30.29,30.22,26.02,23.08,24.67,15.7,14.94,11.21,9.69 51 | 30.66,30.83,30.15,30.61,22.42,22.93,23.72,15.28,13.91,12.04,12.85 52 | 30.68,30.27,30.42,30.65,23.21,21.86,22.86,15.06,13.95,13.94,12.36 53 | 30.34,30.63,30.71,30.67,23.6,21.43,20.34,13.53,14.4,12.97,11.13 54 | 30.67,30.17,30.21,30.34,22.69,20.71,17.6,13.7,12.65,13.91,9.07 55 | 30.19,30.58,30.6,30.02,22.24,21.74,17.3,15.35,12.82,13.89,8.99 56 | 30.44,30.64,30.65,28.87,24.12,23.87,17.06,12.62,12.35,11.94,8.95 57 | 30.72,30.82,30.82,31.44,25.06,25.3,18.43,14.23,12.68,10.92,7.47 58 | 30.86,30.26,30.41,30.57,24.91,25.65,19.72,12.62,12.28,11.46,4.23 59 | 30.78,30.63,30.55,30.28,25.45,26.33,22.73,14.23,14.14,6.23,3.11 60 | 30.89,30.32,30.28,30.14,24.61,25.54,23.86,15.53,14,16.61,2.05 61 | 30.3,30.5,30.48,30.27,25.3,26.63,24.31,15.69,12.94,14.25,2.02 -------------------------------------------------------------------------------- /docs/benchmarks/dut_scripts/check_nic_irq_affinity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | def main(): 6 | if len(sys.argv) != 3: 7 | print("./check_nic_irq_affinity.py ") 8 | sys.exit(1) 9 | 10 | irq_start = sys.argv[1] 11 | irq_end = sys.argv[2] 12 | 13 | irq_to_cpu(int(irq_start), int(irq_end)) 14 | 15 | 16 | def irq_to_cpu(irq_start, irq_end): 17 | for i in range(irq_start, irq_end+1): 18 | raw = open("/proc/irq/{}/smp_affinity".format(i)).read() 19 | hexadecimal = raw.replace("\n", "") 20 | binary = bin(int(hexadecimal, 16)) 21 | cpus = bin_mask_to_index(str(binary)[2:]) 22 | 23 | cpus_str = map(str, cpus) 24 | cpus_str_h = ",".join(cpus_str) 25 | 26 | print("irq: {} - cpus: {}".format(i, cpus_str_h)) 27 | 28 | def bin_mask_to_index(mask): 29 | idx = [] 30 | 31 | mask_rev = mask[::-1] 32 | count = 0 33 | 34 | for s in mask_rev: 35 | if s == "1": 36 | idx.append(count) 37 | 38 | count += 1 39 | 40 | return idx 41 | 42 | 43 | 44 | if __name__ == "__main__": 45 | main() 46 | -------------------------------------------------------------------------------- /docs/benchmarks/dut_scripts/cpus_id: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | def main(): 6 | if len(sys.argv) != 4: 7 | print("./cpu_id ") 8 | print("./cpu_id 235 266 3") 9 | sys.exit(1) 10 | 11 | irq_start = int(sys.argv[1]) 12 | irq_end = int(sys.argv[2]) 13 | no_cpus = int(sys.argv[3]) 14 | 15 | cpus = irq_to_cpu(irq_start, irq_end) 16 | 17 | count = 0 18 | for i in range(no_cpus): 19 | print(cpus[i], end="") 20 | if i+1 != no_cpus: 21 | print(",", end="") 22 | 23 | count += 1 24 | 25 | 26 | def irq_to_cpu(irq_start, irq_end): 27 | cpus_return = [] 28 | for i in range(irq_start, irq_end+1): 29 | raw = open("/proc/irq/{}/smp_affinity".format(i)).read() 30 | hexadecimal = raw.replace("\n", "") 31 | binary = bin(int(hexadecimal, 16)) 32 | cpus = bin_mask_to_index(str(binary)[2:]) 33 | 34 | cpus_str = map(str, cpus) 35 | cpus_str_h = ",".join(cpus_str) 36 | cpus_return.append(cpus_str_h) 37 | 38 | return cpus_return 39 | 40 | def bin_mask_to_index(mask): 41 | idx = [] 42 | 43 | mask_rev = mask[::-1] 44 | count = 0 45 | 46 | for s in mask_rev: 47 | if s == "1": 48 | idx.append(count) 49 | 50 | count += 1 51 | 52 | return idx 53 | 54 | if __name__ == "__main__": 55 | main() 56 | 57 | -------------------------------------------------------------------------------- /docs/benchmarks/dut_scripts/mmwatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import time 5 | import subprocess 6 | import datetime 7 | import re 8 | 9 | 10 | digits_re = re.compile("([0-9eE.+]*)") 11 | to = 2.0 12 | CLS='\033[2J\033[;H' 13 | digit_chars = set('0123456789.') 14 | 15 | 16 | def isfloat(v): 17 | try: 18 | float(v) 19 | except ValueError: 20 | return False 21 | return True 22 | 23 | def total_seconds(td): 24 | return (td.microseconds + (td.seconds + td.days * 24. * 3600) * 10**6) / 10**6 25 | 26 | def main(cmd): 27 | prevp = [] 28 | prevt = None 29 | 30 | while True: 31 | t0 = datetime.datetime.now() 32 | out = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0] 33 | 34 | p = digits_re.split(out.decode()) 35 | 36 | if len(prevp) != len(p): 37 | s = p 38 | else: 39 | s = [] 40 | i = 0 41 | for i, (n, o) in enumerate(zip(p, prevp)): 42 | if isfloat(n) and isfloat(o) and float(n) > float(o): 43 | td = t0 - prevt 44 | v = (float(n) - float(o)) / total_seconds(td) 45 | if v > 1000000000: 46 | v, suffix = v / 1000000000., 'g' 47 | elif v > 1000000: 48 | v, suffix = v / 1000000., 'm' 49 | elif v > 1000: 50 | v, suffix = v / 1000.,'k' 51 | else: 52 | suffix = '' 53 | s.append('\x1b[7m') 54 | s.append('%*s' % (len(n), '%.1f%s/s' % (v, suffix))) 55 | s.append('\x1b[0m') 56 | else: 57 | s.append(n) 58 | s += n[i:] 59 | 60 | prefix = "%sEvery %.1fs: %s\t\t%s" % (CLS, to, ' '.join(cmd), t0) 61 | sys.stdout.write(prefix + '\n\n' + ''.join(s).rstrip() + '\n') 62 | sys.stdout.flush() 63 | 64 | prevt = t0 65 | prevp = p 66 | time.sleep(to) 67 | 68 | if __name__ == '__main__': 69 | try: 70 | main(sys.argv[1:]) 71 | except KeyboardInterrupt: 72 | print('Interrupted') 73 | sys.exit(0) 74 | except SystemExit: 75 | os._exit(0) 76 | -------------------------------------------------------------------------------- /docs/benchmarks/dut_scripts/rtwatch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Modifed from: 4 | # https://github.com/cloudflare/cloudflare-blog/blob/master/2017-06-29-ssdp/mmwatch 5 | 6 | import sys 7 | import time 8 | import subprocess 9 | import datetime 10 | import re 11 | 12 | 13 | digits_re = re.compile("([0-9eE.+]*)") 14 | to = 1.0 15 | CLS='\033[2J\033[;H' 16 | digit_chars = set('0123456789.') 17 | human = False 18 | user_mode = True 19 | 20 | def isfloat(v): 21 | try: 22 | float(v) 23 | except ValueError: 24 | return False 25 | return True 26 | 27 | def total_seconds(td): 28 | return (td.microseconds + (td.seconds + td.days * 24. * 3600) * 10**6) / 10**6 29 | 30 | def main(cmd): 31 | prevp = [] 32 | prevt = None 33 | 34 | while True: 35 | t0 = datetime.datetime.now() 36 | out = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE).communicate()[0] 37 | 38 | p = digits_re.split(out.decode()) 39 | 40 | if len(prevp) != len(p): 41 | s = p 42 | else: 43 | s = [] 44 | i = 0 45 | for i, (n, o) in enumerate(zip(p, prevp)): 46 | if isfloat(n) and isfloat(o) and float(n) > float(o): 47 | td = t0 - prevt 48 | v = (float(n) - float(o)) / total_seconds(td) 49 | 50 | if human: 51 | if v > 1000000000: 52 | v, suffix = v / 1000000000., 'g' 53 | elif v > 1000000: 54 | v, suffix = v / 1000000., 'm' 55 | elif v > 1000: 56 | v, suffix = v / 1000.,'k' 57 | else: 58 | suffix = '' 59 | else: 60 | suffix = '' 61 | 62 | if user_mode: 63 | s.append('\x1b[7m') 64 | 65 | s.append('%*s' % (len(n), '%.1f%s/s' % (v, suffix))) 66 | 67 | if user_mode: 68 | s.append('\x1b[0m') 69 | else: 70 | s.append(n) 71 | s += n[i:] 72 | 73 | if user_mode: 74 | prefix = "%sEvery %.1fs: %s\t\t%s" % (CLS, to, cmd, t0) 75 | sys.stdout.write(prefix + '\n\n' + ''.join(s).rstrip() + '\n') 76 | else: 77 | sys.stdout.write(''.join(s).rstrip() + '\n') 78 | 79 | sys.stdout.flush() 80 | 81 | prevt = t0 82 | prevp = p 83 | time.sleep(to) 84 | 85 | if __name__ == '__main__': 86 | try: 87 | if len(sys.argv) != 5: 88 | print("./rtwatch ") 89 | print("Example: ./rtwatch 1 human no \"ifconfig eno1np0\"") 90 | sys.exit(1) 91 | 92 | to = int(sys.argv[1]) 93 | if sys.argv[2] == "human": 94 | human = True 95 | 96 | if sys.argv[3] == "no": 97 | CLS = "" 98 | user_mode = False 99 | 100 | command = sys.argv[4] 101 | 102 | main(command) 103 | except KeyboardInterrupt: 104 | print('Interrupted') 105 | sys.exit(0) 106 | except SystemExit: 107 | sys.exit(0) 108 | 109 | -------------------------------------------------------------------------------- /docs/benchmarks/dut_scripts/setup_rxqueues.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | DOC="Script to setup ethtool filter steering to RX-queues" 3 | 4 | if [ -z "$1" ]; then 5 | echo $DOC 6 | echo "Usage: $0 DEVICE" 7 | exit 1 8 | fi 9 | IFACE=$1 10 | 11 | START_PORT=2024 12 | NUM_RINGS=$(ethtool -n $IFACE| egrep '[0-9]+ RX rings available' | cut -f 1 -d ' ') 13 | 14 | for ring in $(seq 0 $(($NUM_RINGS - 1))); do 15 | port=$((START_PORT + $ring)) 16 | ethtool -N $IFACE flow-type udp4 src-port $port action $ring 17 | done 18 | -------------------------------------------------------------------------------- /docs/benchmarks/trex/ospa_benchmark.py: -------------------------------------------------------------------------------- 1 | # Modified from xdp-paper 2 | # https://github.com/tohojo/xdp-paper/blob/master/benchmarks/udp_for_benchmarks.py 3 | # to also vary UDP sport when running multiple streams 4 | from trex_stl_lib.api import * 5 | 6 | # Tunables: 7 | # * d_stream_count = DoS streams 8 | # * l_stream_count = Legit streams 9 | # * l_pps = Legit packets per second 10 | # * l_payload = Legit stream payload path (e.g. ./ospa/ospa_req_legit.bin) 11 | # 12 | 13 | class STLS1(object): 14 | def create_stream(self, d_stream_count, l_stream_count, l_payload, l_pps): 15 | packets = [] 16 | 17 | for i in range(l_stream_count): 18 | base_pkt = Ether()/IP(src="10.229.220.2",dst="10.229.220.1")/UDP(dport=22211,sport=2024+i) 19 | 20 | f = open(l_payload, "rb") 21 | data = f.read() 22 | f.close() 23 | 24 | base_pkt /= data 25 | packets.append(STLStream( 26 | packet = STLPktBuilder(pkt = base_pkt), 27 | mode = STLTXCont(pps = l_pps) 28 | )) 29 | 30 | 31 | for i in range(d_stream_count): 32 | base_pkt = Ether()/IP(src="10.229.220.2",dst="10.229.220.1")/UDP(dport=22211,sport=2024+i) 33 | base_pkt /= self.get_dos_ospa_packet_data() 34 | packets.append(STLStream( 35 | packet = STLPktBuilder(pkt = base_pkt), 36 | mode = STLTXCont() 37 | )) 38 | 39 | return packets 40 | 41 | def get_streams(self, d_stream_count = 1, l_stream_count = 1, l_pps = 1, l_payload="./ospa/ospa_req_legit.bin", **kwargs): 42 | return self.create_stream(d_stream_count, l_stream_count, l_pps, l_payload) 43 | 44 | def get_dos_ospa_packet_data(self): 45 | return bytes([ 46 | 0x20, 0x42, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, # OpenSPA header (request, v2, RSA cipher) 47 | 0x01, 0x02, 0x42, 0x24, # Encrypted Payload TLV entry 48 | # Encrypted session (decrypt using RSA) TLV entry 49 | 0x02, 0x24, 50 | 0x24, 0x42, 0x32, 0x77, 0xf1, 0xab, 0x97, 0x11, 51 | 0x72, 0x30, 0x89, 0xa3, 0x47, 0x58, 0x47, 0x32, 52 | 0xa8, 0x04, 0x3c, 0x75, 0x09, 0x45, 0x9b, 0x80, 53 | 0x43, 0x5f, 0x03, 0x47, 0x0e, 0x9f, 0xe3, 0xff, 54 | 0x50, 0x21, 0x08, 0x44 55 | ]) 56 | 57 | # dynamic load - used for trex console or simulator 58 | def register(): 59 | return STLS1() 60 | 61 | -------------------------------------------------------------------------------- /docs/benchmarks/trex/ospa_dos_for_benchmarks.py: -------------------------------------------------------------------------------- 1 | # Modified from xdp-paper 2 | # https://github.com/tohojo/xdp-paper/blob/master/benchmarks/udp_for_benchmarks.py 3 | # to also vary UDP sport when running multiple streams 4 | from trex_stl_lib.api import * 5 | 6 | class STLS1(object): 7 | def create_stream (self, packet_len, stream_count): 8 | packets = [] 9 | for i in range(stream_count): 10 | base_pkt = Ether()/IP(src="10.229.220.2",dst="10.229.220.1")/UDP(dport=22211,sport=2024+i) 11 | base_pkt_len = len(base_pkt) 12 | base_pkt /= self.get_dos_ospa_packet_data() 13 | #base_pkt /= 'x' * max(0, packet_len - base_pkt_len) 14 | packets.append(STLStream( 15 | packet = STLPktBuilder(pkt = base_pkt), 16 | mode = STLTXCont() 17 | )) 18 | return packets 19 | 20 | def get_streams (self, direction = 0, packet_len = 64, stream_count = 1, **kwargs): 21 | # create 1 stream 22 | return self.create_stream(packet_len - 4, stream_count) 23 | 24 | def get_dos_ospa_packet_data(self): 25 | return bytes([ 26 | 0x20, 0x42, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, # OpenSPA header (request, v2, RSA cipher) 27 | 0x01, 0x02, 0x42, 0x24, # Encrypted Payload TLV entry 28 | # Encrypted session (decrypt using RSA) TLV entry 29 | 0x02, 0x24, 30 | 0x24, 0x42, 0x32, 0x77, 0xf1, 0xab, 0x97, 0x11, 31 | 0x72, 0x30, 0x89, 0xa3, 0x47, 0x58, 0x47, 0x32, 32 | 0xa8, 0x04, 0x3c, 0x75, 0x09, 0x45, 0x9b, 0x80, 33 | 0x43, 0x5f, 0x03, 0x47, 0x0e, 0x9f, 0xe3, 0xff, 34 | 0x50, 0x21, 0x08, 0x44 35 | ]) 36 | 37 | # dynamic load - used for trex console or simulator 38 | def register(): 39 | return STLS1() 40 | 41 | -------------------------------------------------------------------------------- /docs/benchmarks/trex/ospa_legit_for_benchmarks.py: -------------------------------------------------------------------------------- 1 | # Modified from xdp-paper 2 | # https://github.com/tohojo/xdp-paper/blob/master/benchmarks/udp_for_benchmarks.py 3 | # to also vary UDP sport when running multiple streams 4 | from trex_stl_lib.api import * 5 | 6 | class STLS1(object): 7 | def create_stream (self, packet_len, stream_count): 8 | packets = [] 9 | for i in range(stream_count): 10 | base_pkt = Ether()/IP(src="10.229.220.3",dst="10.229.220.1")/UDP(dport=22211,sport=2024+i) 11 | base_pkt_len = len(base_pkt) 12 | base_pkt /= self.get_ospa_packet_data() 13 | #base_pkt /= 'x' * max(0, packet_len - base_pkt_len) 14 | packets.append(STLStream( 15 | packet = STLPktBuilder(pkt = base_pkt), 16 | mode = STLTXCont() 17 | )) 18 | return packets 19 | 20 | def get_streams (self, direction = 0, packet_len = 64, stream_count = 1, **kwargs): 21 | # create 1 stream 22 | return self.create_stream(packet_len - 4, stream_count) 23 | 24 | def get_ospa_packet_data(self): 25 | f = open("./ospa/ospa_req_legit.bin", "rb") 26 | data = f.read() 27 | f.close() 28 | return data 29 | 30 | # dynamic load - used for trex console or simulator 31 | def register(): 32 | return STLS1() 33 | 34 | -------------------------------------------------------------------------------- /docs/benchmarks/trex/ospa_req_legit.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenstatic/openspa/edc748cfbcd34acb2865e24fdd1430c941bed0e5/docs/benchmarks/trex/ospa_req_legit.bin -------------------------------------------------------------------------------- /examples/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | python3 ./authorization/authorization_test.py 4 | -------------------------------------------------------------------------------- /examples/authorization/authorization.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Copyright 2022 Gregor Krmelj 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | # documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | # to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | # 10 | # The above copyright notice and this permission notice shall be included in all copies or substantial portions of the 11 | # Software. 12 | # 13 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE 14 | # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 15 | # COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 16 | # OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | 18 | import json 19 | import sys 20 | from typing import TextIO 21 | 22 | 23 | def main(): 24 | try: 25 | ai = get_authorize_input(sys.stdin) 26 | except json.JSONDecodeError as exp: 27 | return fatal("json decode error:" + str(exp)) 28 | 29 | valid, err = ai.valid() 30 | if not valid: 31 | fatal("authorization input invalid: " + err) 32 | 33 | out = user_authorization(ai) 34 | write_authorize_output(sys.stdout, out) 35 | sys.exit(0) 36 | 37 | 38 | class AuthorizationInput: 39 | clientUUID: str = None 40 | ipIsIPv6: bool = None 41 | clientIP: str = None 42 | targetIP: str = None 43 | targetProtocol: str = None 44 | targetPortStart: int = None 45 | targetPortEnd: int = None 46 | 47 | def valid(self) -> (bool, str): 48 | fx = ["clientUUID", "ipIsIPv6", "clientIP", "targetIP", "targetProtocol", "targetPortStart", "targetPortEnd"] 49 | 50 | for f in fx: 51 | if self.__getattribute__(f) is None: 52 | return False, f + " is None" 53 | 54 | return True, "" 55 | 56 | 57 | class AuthorizationOutput: 58 | duration: int = 0 59 | 60 | def is_authorized(self) -> bool: 61 | return self.duration > 0 62 | 63 | 64 | def user_authorization(ai: AuthorizationInput) -> AuthorizationOutput: 65 | """ 66 | Returns the user's authorized duration in seconds. To signal that the user is not authorized, return 0. 67 | """ 68 | 69 | # Perform no validation, allow all requests for 3 minutes 70 | # You can customize this part as much as you like. E.g. using the AuthorizationInput you can check if the user 71 | # has permission for the requested port. 72 | out = AuthorizationOutput() 73 | out.duration = 3 * 60 74 | return out 75 | 76 | 77 | def get_authorize_input(f: TextIO) -> AuthorizationInput: 78 | f_input = "".join(f.readlines()) 79 | 80 | ai_raw = json.loads(f_input) 81 | ai = AuthorizationInput() 82 | 83 | ai.clientUUID = ai_raw.get("clientUUID") 84 | ai.ipIsIPv6 = ai_raw.get("ipIsIPv6") 85 | ai.clientIP = ai_raw.get("clientIP") 86 | ai.targetIP = ai_raw.get("targetIP") 87 | ai.targetProtocol = ai_raw.get("targetProtocol") 88 | ai.targetPortStart = ai_raw.get("targetPortStart") 89 | ai.targetPortEnd = ai_raw.get("targetPortEnd") 90 | 91 | return ai 92 | 93 | 94 | def write_authorize_output(f: TextIO, out: AuthorizationOutput): 95 | f.write(json.dumps({"duration": out.duration})) 96 | f.flush() 97 | 98 | 99 | def fatal(err: str): 100 | sys.stderr.write(err + "\n") 101 | sys.stderr.flush() 102 | sys.exit(1) 103 | 104 | 105 | if __name__ == "__main__": 106 | main() 107 | -------------------------------------------------------------------------------- /examples/authorization/authorization_test.py: -------------------------------------------------------------------------------- 1 | import io 2 | import unittest 3 | import authorization 4 | 5 | 6 | class TestAuthorization(unittest.TestCase): 7 | def test_user_authorization(self): 8 | out = authorization.user_authorization(authorization.AuthorizationInput()) 9 | self.assertIsInstance(out, authorization.AuthorizationOutput) 10 | self.assertEqual(out.duration, 180) 11 | 12 | def test_get_authorize_input__valid(self): 13 | in_json = """ 14 | { 15 | "clientUUID": "62fcb148-76cf-45d2-9781-a09b95b309d9", 16 | "ipIsIPv6": false, 17 | "clientIP": "88.200.23.23", 18 | "targetIP": "88.200.23.30", 19 | "targetProtocol": "TCP", 20 | "targetPortStart": 80, 21 | "targetPortEnd": 1000 22 | } 23 | """ 24 | f = io.StringIO(in_json) 25 | ai = authorization.get_authorize_input(f) 26 | 27 | self.assertEqual(ai.clientUUID, "62fcb148-76cf-45d2-9781-a09b95b309d9") 28 | self.assertEqual(ai.ipIsIPv6, False) 29 | self.assertEqual(ai.clientIP, "88.200.23.23") 30 | self.assertEqual(ai.targetIP, "88.200.23.30") 31 | self.assertEqual(ai.targetProtocol, "TCP") 32 | self.assertEqual(ai.targetPortStart, 80) 33 | self.assertEqual(ai.targetPortEnd, 1000) 34 | 35 | def test_get_authorize_input__missing_targetPortEnd(self): 36 | in_json = """ 37 | { 38 | "clientUUID": "62fcb148-76cf-45d2-9781-a09b95b309d9", 39 | "ipIsIPv6": false, 40 | "clientIP": "88.200.23.23", 41 | "targetIP": "88.200.23.30", 42 | "targetProtocol": "TCP", 43 | "targetPortStart": 80 44 | } 45 | """ 46 | f = io.StringIO(in_json) 47 | ai = authorization.get_authorize_input(f) 48 | valid, _ = ai.valid() 49 | self.assertFalse(valid) 50 | 51 | def test_write_authorize_output(self): 52 | f = io.StringIO() 53 | out = authorization.AuthorizationOutput() 54 | out.duration = 45 55 | 56 | authorization.write_authorize_output(f, out) 57 | 58 | expect = """{"duration": 45}""" 59 | self.assertEqual(f.getvalue(), expect) 60 | 61 | 62 | if __name__ == '__main__': 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/greenstatic/openspa 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.9.3 7 | github.com/emirpasic/gods v1.18.1 8 | github.com/pkg/errors v0.9.1 9 | github.com/pquerna/otp v1.3.0 10 | github.com/prometheus/client_golang v1.13.0 11 | github.com/prometheus/client_model v0.2.0 12 | github.com/rs/zerolog v1.28.0 13 | github.com/satori/go.uuid v1.2.0 14 | github.com/spf13/cobra v1.5.0 15 | github.com/stretchr/testify v1.8.0 16 | github.com/vishvananda/netlink v1.1.0 17 | gopkg.in/yaml.v2 v2.4.0 18 | ) 19 | 20 | require ( 21 | github.com/beorn7/perks v1.0.1 // indirect 22 | github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect 23 | github.com/cespare/xxhash/v2 v2.1.2 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/golang/protobuf v1.5.2 // indirect 26 | github.com/greenstatic/openspa/internal/xdp v0.0.0-00010101000000-000000000000 // indirect 27 | github.com/inconshreveable/mousetrap v1.0.1 // indirect 28 | github.com/mattn/go-colorable v0.1.12 // indirect 29 | github.com/mattn/go-isatty v0.0.14 // indirect 30 | github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect 31 | github.com/pmezard/go-difflib v1.0.0 // indirect 32 | github.com/prometheus/common v0.37.0 // indirect 33 | github.com/prometheus/procfs v0.8.0 // indirect 34 | github.com/spf13/pflag v1.0.5 // indirect 35 | github.com/stretchr/objx v0.4.0 // indirect 36 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect 37 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 38 | google.golang.org/protobuf v1.28.1 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | ) 41 | 42 | replace github.com/greenstatic/openspa/internal/xdp => ./internal/xdp 43 | -------------------------------------------------------------------------------- /internal/adk_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/greenstatic/openspa/internal/observability" 7 | "github.com/greenstatic/openspa/internal/xdp" 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/mock" 10 | ) 11 | 12 | func TestXDPADKMetrics(t *testing.T) { 13 | m := &statsProviderMock{} 14 | repo := newRepoCounterFuncStub() 15 | 16 | x := newXDPADKMetrics(m) 17 | x.mr = repo 18 | 19 | assert.Equal(t, 0, repo.noRegistered()) 20 | 21 | const noMetrics = 13 22 | // The values should be incremented by one according to the metricNames. This helps us 23 | // test all of these values in a for loop by using the variable mStatsRecordValue which gets incremented by one 24 | // hence the requirement for these values to be incremented by one. 25 | mStats := xdp.Stats{ 26 | XDPAborted: xdp.StatsRecord{Packets: 1, Bytes: 2}, 27 | XDPDrop: xdp.StatsRecord{Packets: 3, Bytes: 4}, 28 | XDPPass: xdp.StatsRecord{Packets: 5, Bytes: 6}, 29 | XDPTX: xdp.StatsRecord{Packets: 7, Bytes: 8}, 30 | XDPRedirect: xdp.StatsRecord{Packets: 9, Bytes: 10}, 31 | OpenSPANot: 11, 32 | OpenSPAADKProofInvalid: 12, 33 | OpenSPAADKProofValid: 13, 34 | } 35 | 36 | m.On("Stats").Return(mStats, nil) 37 | 38 | x.setupMetrics() 39 | assert.Equal(t, noMetrics, repo.noRegistered()) 40 | 41 | metricNames := []string{ 42 | "xdp_aborted_packets", 43 | "xdp_aborted_bytes", 44 | "xdp_drop_packets", 45 | "xdp_drop_bytes", 46 | "xdp_pass_packets", 47 | "xdp_pass_bytes", 48 | "xdp_tx_packets", 49 | "xdp_tx_bytes", 50 | "xdp_redirect_packets", 51 | "xdp_redirect_bytes", 52 | "xdp_openspa_not", 53 | "xdp_openspa_adk_proof_invalid", 54 | "xdp_openspa_adk_proof_valid", 55 | } 56 | 57 | mStatsRecordValue := 1 58 | 59 | for _, name := range metricNames { 60 | f := repo.getCountFuncStub(name) 61 | assert.NotNil(t, f, name) 62 | assert.Equalf(t, float64(mStatsRecordValue), f.fn(), name) 63 | mStatsRecordValue++ 64 | } 65 | 66 | x.teardownMetrics() 67 | assert.Equal(t, 0, repo.noRegistered()) 68 | 69 | m.AssertExpectations(t) 70 | m.AssertNumberOfCalls(t, "Stats", 1) 71 | } 72 | 73 | type statsProviderMock struct { 74 | mock.Mock 75 | } 76 | 77 | func (s *statsProviderMock) Stats() (xdp.Stats, error) { 78 | args := s.Called() 79 | return args.Get(0).(xdp.Stats), args.Error(1) 80 | } 81 | 82 | type repoCounterFuncStub struct { 83 | observability.MetricsRepositoryStub 84 | counters map[string]*counterFuncStub 85 | } 86 | 87 | type counterFuncStub struct { 88 | name string 89 | lbl observability.Labels 90 | fn func() float64 91 | wasRegistered bool 92 | } 93 | 94 | var _ observability.MetricsRepository = &repoCounterFuncStub{} 95 | 96 | func newRepoCounterFuncStub() *repoCounterFuncStub { 97 | r := &repoCounterFuncStub{ 98 | counters: make(map[string]*counterFuncStub), 99 | } 100 | return r 101 | } 102 | 103 | func (r *repoCounterFuncStub) getCountFuncStub(name string) *counterFuncStub { 104 | c, ok := r.counters[name] 105 | if !ok { 106 | return nil 107 | } 108 | return c 109 | } 110 | 111 | func (r *repoCounterFuncStub) CountFunc(name string, l observability.Labels) observability.CounterFunc { 112 | c := &counterFuncStub{ 113 | name: name, 114 | lbl: l, 115 | } 116 | r.counters[name] = c 117 | return c 118 | } 119 | 120 | func (r *repoCounterFuncStub) noRegistered() int { 121 | count := 0 122 | 123 | for _, cf := range r.counters { 124 | if cf.isRegistered() { 125 | count++ 126 | } 127 | } 128 | 129 | return count 130 | } 131 | 132 | var _ observability.CounterFunc = &counterFuncStub{} 133 | 134 | func (c *counterFuncStub) CounterFuncRegister(fn func() float64) { 135 | c.fn = fn 136 | c.wasRegistered = true 137 | } 138 | 139 | func (c *counterFuncStub) CounterFuncDeregister() { 140 | c.fn = nil 141 | } 142 | 143 | func (c *counterFuncStub) isRegistered() bool { 144 | return c.fn != nil 145 | } 146 | -------------------------------------------------------------------------------- /internal/authorization.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "time" 7 | 8 | lib "github.com/greenstatic/openspa/pkg/openspalib" 9 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 10 | "github.com/pkg/errors" 11 | "github.com/rs/zerolog/log" 12 | ) 13 | 14 | type AuthorizationStrategy interface { 15 | RequestAuthorization(request tlv.Container) (time.Duration, error) 16 | } 17 | 18 | var _ AuthorizationStrategy = AuthorizationStrategySimple{} 19 | 20 | // AuthorizationStrategySimple authorizes any form of request as long as it is authenticated successfully 21 | // (authentication should be performed externally). 22 | type AuthorizationStrategySimple struct { 23 | dur time.Duration 24 | } 25 | 26 | func NewAuthorizationStrategyAllow(duration time.Duration) *AuthorizationStrategySimple { 27 | a := &AuthorizationStrategySimple{ 28 | dur: duration, 29 | } 30 | return a 31 | } 32 | 33 | func (a AuthorizationStrategySimple) RequestAuthorization(_ tlv.Container) (time.Duration, error) { 34 | return a.dur, nil 35 | } 36 | 37 | var _ AuthorizationStrategy = AuthorizationStrategyCommand{} 38 | 39 | type AuthorizationStrategyCommand struct { 40 | AuthorizeCmd string 41 | 42 | exec CommandExecuter 43 | } 44 | 45 | type AuthorizationStrategyCommandAuthorizeInput struct { 46 | ClientUUID string `json:"clientUUID"` 47 | IPIsIPv6 bool `json:"ipIsIPv6"` 48 | ClientIP net.IP `json:"clientIP"` 49 | TargetIP net.IP `json:"targetIP"` 50 | TargetProtocol string `json:"targetProtocol"` 51 | TargetPortStart int `json:"targetPortStart"` 52 | TargetPortEnd int `json:"targetPortEnd"` 53 | } 54 | 55 | type AuthorizationStrategyCommandAuthorizeOutput struct { 56 | Duration int `json:"duration"` 57 | } 58 | 59 | func NewAuthorizationStrategyCommand(cmd string) *AuthorizationStrategyCommand { 60 | a := &AuthorizationStrategyCommand{ 61 | AuthorizeCmd: cmd, 62 | 63 | exec: &CommandExecute{}, 64 | } 65 | return a 66 | } 67 | 68 | func (a AuthorizationStrategyCommand) RequestAuthorization(c tlv.Container) (time.Duration, error) { 69 | i, err := a.authorizeInputGenerate(c) 70 | if err != nil { 71 | return 0, err 72 | } 73 | 74 | stdin, err := json.Marshal(i) 75 | if err != nil { 76 | return 0, errors.Wrap(err, "json marshal AuthorizationStrategyCommand stdin input") 77 | } 78 | 79 | stdout, err := a.exec.Execute(a.AuthorizeCmd, stdin) 80 | out := AuthorizationStrategyCommandAuthorizeOutput{} 81 | if err := json.Unmarshal(stdout, &out); err != nil { 82 | log.Info().Msgf("Authorize command output: %s", string(stdout)) 83 | return 0, errors.Wrap(err, "json unmarshal AuthorizationStrategyCommand stdout output") 84 | } 85 | 86 | log.Debug().Msgf("Authorize command output: %s", string(stdout)) 87 | if err != nil { 88 | return 0, errors.Wrap(err, "executing authorization command "+a.AuthorizeCmd) 89 | } 90 | 91 | d := time.Duration(out.Duration) * time.Second 92 | return d, nil 93 | } 94 | 95 | //nolint:lll 96 | func (a AuthorizationStrategyCommand) authorizeInputGenerate(c tlv.Container) (AuthorizationStrategyCommandAuthorizeInput, error) { 97 | fwd, err := lib.RequestFirewallDataFromContainer(c) 98 | if err != nil { 99 | return AuthorizationStrategyCommandAuthorizeInput{}, errors.Wrap(err, "request firewall data from container") 100 | } 101 | 102 | i := AuthorizationStrategyCommandAuthorizeInput{ 103 | ClientUUID: fwd.ClientUUID, 104 | IPIsIPv6: isIPv6(fwd.TargetIP), 105 | ClientIP: fwd.ClientIP, 106 | TargetIP: fwd.TargetIP, 107 | TargetProtocol: fwd.TargetProtocol.String(), 108 | TargetPortStart: fwd.TargetPortStart, 109 | TargetPortEnd: fwd.TargetPortEnd, 110 | } 111 | 112 | return i, nil 113 | } 114 | 115 | func NewAuthorizationStrategyFromServerConfigAuthorization(s ServerConfigAuthorization) (AuthorizationStrategy, error) { 116 | switch s.Backend { 117 | case ServerConfigAuthorizationBackendSimple: 118 | return NewAuthorizationStrategyAllow(s.Simple.GetDuration()), nil 119 | 120 | case ServerConfigAuthorizationBackendCommand: 121 | return NewAuthorizationStrategyCommand(s.Command.AuthorizationCmd), nil 122 | 123 | case ServerConfigAuthorizationBackendNone: 124 | return authorizationStrategyDummy{}, nil 125 | } 126 | 127 | return nil, errors.New("unsupported authorization backend") 128 | } 129 | 130 | // authorizationStrategyDummy does nothing, it is just used to satisfy the interface definition. It is mostly used 131 | // for testing/performance measurement purposes. Do not use for production work. 132 | type authorizationStrategyDummy struct{} 133 | 134 | func (a authorizationStrategyDummy) RequestAuthorization(request tlv.Container) (time.Duration, error) { 135 | return 3 * time.Second, nil 136 | } 137 | -------------------------------------------------------------------------------- /internal/authorization_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | lib "github.com/greenstatic/openspa/pkg/openspalib" 10 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 11 | "github.com/pkg/errors" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestAuthorizationStrategySimple(t *testing.T) { 17 | c := tlv.NewContainerMock() 18 | 19 | dur := time.Hour 20 | as := NewAuthorizationStrategyAllow(dur) 21 | 22 | d, err := as.RequestAuthorization(c) 23 | assert.NoError(t, err) 24 | assert.Equal(t, dur, d) 25 | 26 | c.AssertExpectations(t) 27 | } 28 | 29 | func TestAuthorizationStrategyCommand(t *testing.T) { 30 | c, err := lib.RequestDataToContainer(lib.RequestData{ 31 | TransactionID: 0, 32 | ClientUUID: "0561e333-9428-429c-8ab0-1106dd6e311c", 33 | ClientIP: net.IPv4(88, 200, 23, 22).To4(), 34 | TargetProtocol: lib.ProtocolTCP, 35 | TargetIP: net.IPv4(88, 200, 23, 23).To4(), 36 | TargetPortStart: 80, 37 | TargetPortEnd: 1000, 38 | }, lib.RequestExtendedData{ 39 | Timestamp: time.Now(), 40 | }) 41 | require.NoError(t, err) 42 | 43 | input := AuthorizationStrategyCommandAuthorizeInput{ 44 | ClientUUID: "0561e333-9428-429c-8ab0-1106dd6e311c", 45 | ClientIP: net.IPv4(88, 200, 23, 22).To4(), 46 | TargetProtocol: "TCP", 47 | TargetIP: net.IPv4(88, 200, 23, 23).To4(), 48 | TargetPortStart: 80, 49 | TargetPortEnd: 1000, 50 | } 51 | 52 | inputB, err := json.Marshal(input) 53 | require.NoError(t, err) 54 | 55 | exec := &CommandExecuteMock{} 56 | 57 | out := AuthorizationStrategyCommandAuthorizeOutput{ 58 | Duration: 60, // seconds 59 | } 60 | 61 | stdout, err := json.Marshal(out) 62 | require.NoError(t, err) 63 | 64 | exec.On("Execute", "foo", inputB, []string(nil)).Return(stdout, nil).Once() 65 | 66 | as := NewAuthorizationStrategyCommand("foo") 67 | as.exec = exec 68 | 69 | d, err := as.RequestAuthorization(c) 70 | assert.NoError(t, err) 71 | assert.Equal(t, time.Minute, d) 72 | 73 | exec.AssertExpectations(t) 74 | 75 | exec.On("Execute", "foo", inputB, []string(nil)).Return([]byte{}, errors.New("test error")).Once() 76 | 77 | _, err = as.RequestAuthorization(c) 78 | assert.Error(t, err) 79 | 80 | exec.AssertExpectations(t) 81 | } 82 | 83 | func TestAuthorizationStrategyCommand_AuthorizeInputGenerate(t *testing.T) { 84 | c, err := lib.RequestDataToContainer(lib.RequestData{ 85 | TransactionID: 0, 86 | ClientUUID: "0561e333-9428-429c-8ab0-1106dd6e311c", 87 | ClientIP: net.IPv4(88, 200, 23, 22).To4(), 88 | TargetProtocol: lib.ProtocolTCP, 89 | TargetIP: net.IPv4(88, 200, 23, 23).To4(), 90 | TargetPortStart: 80, 91 | TargetPortEnd: 1000, 92 | }, lib.RequestExtendedData{ 93 | Timestamp: time.Now(), 94 | }) 95 | require.NoError(t, err) 96 | 97 | inputExpect := AuthorizationStrategyCommandAuthorizeInput{ 98 | ClientUUID: "0561e333-9428-429c-8ab0-1106dd6e311c", 99 | IPIsIPv6: false, 100 | ClientIP: net.IPv4(88, 200, 23, 22).To4(), 101 | TargetIP: net.IPv4(88, 200, 23, 23).To4(), 102 | TargetProtocol: FirewallProtoTCP, 103 | TargetPortStart: 80, 104 | TargetPortEnd: 1000, 105 | } 106 | 107 | as := AuthorizationStrategyCommand{} 108 | input, err := as.authorizeInputGenerate(c) 109 | assert.NoError(t, err) 110 | assert.Equal(t, inputExpect, input) 111 | } 112 | -------------------------------------------------------------------------------- /internal/client_mocks_and_stubs.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "net" 5 | "time" 6 | 7 | lib "github.com/greenstatic/openspa/pkg/openspalib" 8 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 9 | "github.com/pkg/errors" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | type udpSenderMock struct { 14 | mock.Mock 15 | } 16 | 17 | func (u *udpSenderMock) SendUDPRequest(req []byte, dest net.UDPAddr, timeout time.Duration) ([]byte, error) { 18 | args := u.Called(req, dest, timeout) 19 | return args.Get(0).([]byte), args.Error(1) 20 | } 21 | 22 | type udpSenderStubServer struct { 23 | responderParams stubServerResponderParams 24 | cs crypto.CipherSuite 25 | preHook func(req []byte, dest net.UDPAddr, timeout time.Duration) 26 | } 27 | 28 | func (u *udpSenderStubServer) SendUDPRequest(req []byte, dest net.UDPAddr, timeout time.Duration) ([]byte, error) { 29 | if u.preHook != nil { 30 | u.preHook(req, dest, timeout) 31 | } 32 | 33 | resp, err := stubServerResponder(req, u.cs, u.responderParams) 34 | if err != nil { 35 | return nil, errors.Wrap(err, "stub server responder") 36 | } 37 | 38 | respB, err := resp.Marshal() 39 | if err != nil { 40 | return nil, errors.Wrap(err, "response marshal") 41 | } 42 | 43 | return respB, nil 44 | } 45 | 46 | type stubServerResponderParams struct { 47 | Duration time.Duration 48 | } 49 | 50 | func stubServerResponder(reqB []byte, cs crypto.CipherSuite, params stubServerResponderParams) (*lib.Response, error) { 51 | req, err := lib.RequestUnmarshal(reqB, cs) 52 | if err != nil { 53 | return nil, errors.Wrap(err, "request unmarshal") 54 | } 55 | 56 | clientUUID, err := lib.ClientUUIDFromContainer(req.Body) 57 | if err != nil { 58 | return nil, errors.Wrap(err, "request has no client uuid") 59 | } 60 | 61 | firewallC, err := lib.TLVFromContainer(req.Body, lib.FirewallKey) 62 | if err != nil { 63 | return nil, errors.Wrap(err, "firewall tlv from container") 64 | } 65 | 66 | if firewallC == nil { 67 | return nil, errors.New("firewall tlv container nil") 68 | } 69 | 70 | targetProto, err := lib.TargetProtocolFromContainer(firewallC) 71 | if err != nil { 72 | return nil, errors.Wrap(err, "target protocol from container") 73 | } 74 | 75 | targetIP, err := lib.TargetIPFromContainer(firewallC) 76 | if err != nil { 77 | return nil, errors.Wrap(err, "target ip from container") 78 | } 79 | 80 | targetPortStart, err := lib.TargetPortStartFromContainer(firewallC) 81 | if err != nil { 82 | return nil, errors.Wrap(err, "target port start from container") 83 | } 84 | 85 | targetPortEnd, err := lib.TargetPortEndFromContainer(firewallC) 86 | if err != nil { 87 | return nil, errors.Wrap(err, "target port end from container") 88 | } 89 | 90 | resp, err := lib.NewResponse(lib.ResponseData{ 91 | TransactionID: req.Header.TransactionID, 92 | TargetProtocol: targetProto, 93 | TargetIP: targetIP, 94 | TargetPortStart: targetPortStart, 95 | TargetPortEnd: targetPortEnd, 96 | Duration: params.Duration, 97 | ClientUUID: clientUUID, 98 | }, cs) 99 | if err != nil { 100 | return nil, errors.Wrap(err, "new response") 101 | } 102 | 103 | return resp, nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/cmd/adk.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/greenstatic/openspa/pkg/openspalib" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var ADKCmd = &cobra.Command{ 12 | Use: "adk", 13 | Short: "Anti DoS Knocking utilities", 14 | Run: adkCmdRunFn, 15 | PreRun: PreRunLogSetupFn, 16 | } 17 | 18 | var ADKSecretCmd = &cobra.Command{ 19 | Use: "secret", 20 | Short: "Generate ADK (encoded) secret", 21 | Run: adkSecretCmdRunFn, 22 | PreRun: PreRunLogSetupFn, 23 | } 24 | 25 | var ADKProofCmd = &cobra.Command{ 26 | Use: "proof ", 27 | Short: "Generate ADK proof", 28 | Run: adkProofCmdRunFn, 29 | PreRun: PreRunLogSetupFn, 30 | Args: cobra.ExactArgs(1), 31 | } 32 | 33 | func ADKCmdSetup(c *cobra.Command) { 34 | c.AddCommand(ADKSecretCmd) 35 | c.AddCommand(ADKProofCmd) 36 | } 37 | 38 | func adkCmdRunFn(cmd *cobra.Command, args []string) { 39 | _ = cmd.Help() 40 | } 41 | 42 | func adkSecretCmdRunFn(cmd *cobra.Command, args []string) { 43 | secret, err := openspalib.ADKGenerateSecret() 44 | if err != nil { 45 | fmt.Fprintf(os.Stderr, "failed to generate ADK secret, err: %s\n", err) 46 | os.Exit(1) 47 | } 48 | fmt.Fprintf(os.Stdout, "Secret: %s\n", secret) 49 | } 50 | 51 | func adkProofCmdRunFn(cmd *cobra.Command, args []string) { 52 | if len(args) != 1 { 53 | fmt.Fprintf(os.Stderr, "invalid number of arguments\n") 54 | os.Exit(1) 55 | } 56 | 57 | secret := args[0] 58 | 59 | proof, err := openspalib.ADKGenerateProof(secret) 60 | if err != nil { 61 | fmt.Fprintf(os.Stderr, "failed to generate ADK proof, err: %s\n", err) 62 | os.Exit(1) 63 | } 64 | fmt.Fprintf(os.Stdout, "Proof: %d\n", proof) 65 | } 66 | -------------------------------------------------------------------------------- /internal/cmd/helpers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/rs/zerolog" 7 | "github.com/rs/zerolog/log" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func RootCmdSetupFlags(cmd *cobra.Command) { 12 | cmd.PersistentFlags().BoolP("verbose", "v", false, "verbose logging (debug level and higher)") 13 | } 14 | 15 | func PreRunLogSetupFn(cmd *cobra.Command, args []string) { 16 | verbose, _ := cmd.Flags().GetBool("verbose") 17 | LogSetup(verbose) 18 | } 19 | 20 | func LogSetup(verbose bool) { 21 | log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout}).Level(zerolog.InfoLevel) 22 | if verbose { 23 | log.Logger = log.Logger.Level(zerolog.DebugLevel) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/cmd/ip.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/greenstatic/openspa/internal" 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | var IPCmd = &cobra.Command{ 12 | Use: "ip", 13 | Short: "Returns the client's public IPv4 and IPv6 address", 14 | Run: ipCmdRunFn, 15 | PreRun: PreRunLogSetupFn, 16 | } 17 | 18 | func IPCmdSetup(c *cobra.Command) { 19 | c.Flags().StringP("ipv4-server", "4", internal.IPv4ServerDefault, 20 | "The server to use to resolve client's public IPv4 address (needs to be a URL)") 21 | 22 | c.Flags().StringP("ipv6-server", "6", internal.IPv6ServerDefault, 23 | "The server to use to resolve client's public IPv6 address (needs to be a URL)") 24 | } 25 | 26 | func ipCmdRunFn(cmd *cobra.Command, args []string) { 27 | v4, err := cmd.Flags().GetString("ipv4-server") 28 | if err != nil { 29 | fmt.Fprintf(os.Stderr, "invalid ipv4-server, err: %s\n", err) 30 | os.Exit(1) 31 | } 32 | v6, err := cmd.Flags().GetString("ipv6-server") 33 | if err != nil { 34 | fmt.Fprintf(os.Stderr, "invalid ipv6-server, err: %s\n", err) 35 | os.Exit(1) 36 | } 37 | 38 | internal.GetIP(v4, v6) 39 | } 40 | -------------------------------------------------------------------------------- /internal/cmd/server.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "net" 5 | "os" 6 | "os/signal" 7 | "syscall" 8 | 9 | "github.com/greenstatic/openspa/internal" 10 | "github.com/greenstatic/openspa/internal/xdp" 11 | "github.com/pkg/errors" 12 | "github.com/rs/zerolog/log" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | var ServerCmd = &cobra.Command{ 17 | Use: "server", 18 | Short: "Start OpenSPA server", 19 | Run: serverCmdRunFn, 20 | PreRun: PreRunLogSetupFn, 21 | } 22 | 23 | func ServerCmdSetup(c *cobra.Command) { 24 | c.Flags().StringP("config", "c", "config.yaml", "Server configuration file") 25 | } 26 | 27 | func serverCmdRunFn(cmd *cobra.Command, args []string) { 28 | configFilePath, err := cmd.Flags().GetString("config") 29 | if err != nil { 30 | log.Fatal().Err(err).Msgf("Failed to get config file path") 31 | } 32 | 33 | cBytes, err := os.ReadFile(configFilePath) 34 | if err != nil { 35 | log.Fatal().Err(err).Msgf("Failed to read config file") 36 | } 37 | 38 | sc, err := internal.ServerConfigParse(cBytes) 39 | if err != nil { 40 | log.Fatal().Err(err).Msgf("Failed to parse config file") 41 | } 42 | 43 | if err := sc.Verify(); err != nil { 44 | log.Fatal().Err(err).Msgf("Server config file invalid") 45 | } 46 | 47 | server(cmd, sc) 48 | } 49 | 50 | func server(_ *cobra.Command, config internal.ServerConfig) { 51 | sigs := make(chan os.Signal, 1) 52 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 53 | done := make(chan bool, 1) 54 | 55 | xdkMetricsStop := make(chan bool) 56 | xadk, err := xdpSetup(config, xdkMetricsStop) 57 | if err != nil { 58 | log.Fatal().Err(err).Msgf("ADK/XDP setup error") 59 | } 60 | 61 | cs, err := internal.NewServerCipherSuite(config.Crypto) 62 | if err != nil { 63 | log.Fatal().Err(err).Msgf("Failed to setup server cipher suite") 64 | } 65 | 66 | fw, err := internal.NewFirewallFromServerConfigFirewall(config.Firewall) 67 | if err != nil { 68 | log.Fatal().Err(err).Msgf("Failed to initialize firewall backend") 69 | } 70 | 71 | authz, err := internal.NewAuthorizationStrategyFromServerConfigAuthorization(config.Authorization) 72 | if err != nil { 73 | log.Fatal().Err(err).Msgf("Failed to initialize authorization backend") 74 | } 75 | 76 | httpIP, httpPort := serverHTTPServerSettingsFromConfig(config) 77 | 78 | s := internal.NewServer(internal.ServerSettings{ 79 | UDPServerIP: net.ParseIP(config.Server.IP), 80 | UDPServerPort: config.Server.Port, 81 | NoRequestHandlers: config.Server.RequestHandlers, 82 | FW: fw, 83 | CS: cs, 84 | Authz: authz, 85 | ADKSecret: config.Server.ADK.Secret, 86 | HTTPServerIP: httpIP, 87 | HTTPServerPort: httpPort, 88 | }) 89 | 90 | if xadk != nil { 91 | if err := xadk.Start(); err != nil { 92 | log.Fatal().Err(err).Msgf("XDP/ADK start error") 93 | } 94 | } 95 | 96 | go func() { 97 | sig := <-sigs 98 | log.Info().Msgf("Received signal %s", sig.String()) 99 | done <- true 100 | }() 101 | 102 | go func() { 103 | if err := s.Start(); err != nil { 104 | log.Fatal().Err(err).Msgf("Server error") 105 | } 106 | }() 107 | 108 | <-done 109 | 110 | if xadk != nil { 111 | log.Info().Msgf("Stopping XDP/ADK") 112 | if err := xadk.Stop(); err != nil { 113 | log.Error().Err(err).Msgf("Failed to stop XDP/ADK") 114 | } 115 | xdkMetricsStop <- true 116 | } 117 | 118 | log.Info().Msgf("Stopping server") 119 | if err := s.Stop(); err != nil { 120 | log.Error().Err(err).Msgf("Server stop") 121 | } 122 | log.Info().Msgf("Successfully stopped server") 123 | } 124 | 125 | func serverHTTPServerSettingsFromConfig(config internal.ServerConfig) (net.IP, int) { 126 | port := config.Server.HTTP.Port 127 | if !config.Server.HTTP.Enable { 128 | port = 0 129 | } 130 | 131 | return net.ParseIP(config.Server.HTTP.IP), port 132 | } 133 | 134 | func xdpADKEnabled(config internal.ServerConfig) bool { 135 | return config.Server.ADK.XDP.Mode != "" 136 | } 137 | 138 | func xdpPrecheck(config internal.ServerConfig) error { 139 | if !xdpADKEnabled(config) { 140 | return nil 141 | } 142 | 143 | if !xdp.IsSupported() { 144 | return errors.New("xdp is not supported in this build") 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func xdpSetup(config internal.ServerConfig, metricsStop chan bool) (xdp.ADK, error) { 151 | if err := xdpPrecheck(config); err != nil { 152 | return nil, errors.Wrap(err, "xdp precheck") 153 | } 154 | 155 | if !xdpADKEnabled(config) { 156 | return nil, nil 157 | } 158 | 159 | log.Info().Msgf("Setting up XDP ADK acceleration") 160 | 161 | xdpConf := config.Server.ADK.XDP 162 | mode, ok := xdp.ModeFromString(xdpConf.Mode) 163 | if !ok { 164 | return nil, errors.New("unsupported mode") 165 | } 166 | 167 | iName := xdpConf.Interfaces[0] // currently we only support a single interface 168 | 169 | set := xdp.ADKSettings{ 170 | InterfaceName: iName, 171 | Mode: mode, 172 | ReplaceIfLoaded: true, 173 | UDPServerPort: config.Server.Port, 174 | } 175 | 176 | adk, err := xdp.NewADK(set, internal.NewADKProofGen(config.Server.ADK.Secret)) 177 | if err != nil { 178 | return nil, errors.Wrap(err, "new adk") 179 | } 180 | 181 | internal.SetupXDPADKMetrics(adk, metricsStop) 182 | 183 | return adk, nil 184 | } 185 | -------------------------------------------------------------------------------- /internal/cmd/version.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/greenstatic/openspa/internal" 7 | "github.com/greenstatic/openspa/internal/xdp" 8 | lib "github.com/greenstatic/openspa/pkg/openspalib" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func VersionCmdGet(withADKXDPLine bool) *cobra.Command { 13 | return &cobra.Command{ 14 | Use: "version", 15 | Run: func(cmd *cobra.Command, args []string) { 16 | fmt.Println("THIS IS PROTOTYPE SOFTWARE") 17 | fmt.Printf("OpenSPA version: %s\n", internal.Version()) 18 | fmt.Printf("OpenSPA Protocol version: %s\n", lib.Version()) 19 | 20 | if withADKXDPLine { 21 | fmt.Printf("adk XDP support: %t\n", xdp.IsSupported()) 22 | } 23 | }, 24 | PreRun: PreRunLogSetupFn, 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /internal/common.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "os/exec" 6 | ) 7 | 8 | type CommandExecuter interface { 9 | Execute(cmd string, stdin []byte, args ...string) ([]byte, error) 10 | } 11 | 12 | var _ CommandExecuter = &CommandExecute{} 13 | 14 | type CommandExecute struct{} 15 | 16 | func (c *CommandExecute) Execute(cmd string, stdin []byte, args ...string) ([]byte, error) { 17 | cmnd := exec.Command(cmd, args...) 18 | cmnd.Stdin = bytes.NewBuffer(stdin) 19 | 20 | out, err := cmnd.Output() 21 | if err != nil { 22 | return nil, execErrHandle(err) 23 | } 24 | 25 | return out, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/common_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCommandExecute_Stdout(t *testing.T) { 10 | exec := CommandExecute{} 11 | 12 | b, err := exec.Execute("echo", []byte{}, "one", "two", "three") 13 | assert.NoError(t, err) 14 | assert.Equal(t, "one two three\n", string(b)) 15 | } 16 | 17 | func TestCommandExecute_Stdin(t *testing.T) { 18 | exec := CommandExecute{} 19 | 20 | b, err := exec.Execute("cat", []byte("one two three")) 21 | assert.NoError(t, err) 22 | assert.Equal(t, "one two three", string(b)) 23 | } 24 | -------------------------------------------------------------------------------- /internal/firewall.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "time" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | var ( 12 | FirewallProtoTCP = "TCP" 13 | FirewallProtoUDP = "UDP" 14 | FirewallProtoICMP = "ICMP" 15 | FirewallProtoICMPv6 = "ICMPv6" 16 | ) 17 | 18 | type FirewallRule struct { 19 | Proto string 20 | SrcIP net.IP 21 | DstIP net.IP 22 | DstPortStart int 23 | DstPortEnd int 24 | } 25 | 26 | type FirewallRuleMetadata struct { 27 | ClientUUID string 28 | Duration time.Duration 29 | } 30 | 31 | type Firewall interface { 32 | FirewallSetup() error 33 | RuleAdd(r FirewallRule, meta FirewallRuleMetadata) error 34 | RuleRemove(r FirewallRule, meta FirewallRuleMetadata) error 35 | } 36 | 37 | func (r *FirewallRule) String() string { 38 | s := fmt.Sprintf("%s -> %s %s/%d", r.SrcIP.String(), r.DstIP.String(), r.Proto, r.DstPortStart) 39 | if r.DstPortEnd != r.DstPortStart && r.DstPortEnd != 0 { 40 | return fmt.Sprintf("%s-%d", s, r.DstPortEnd) 41 | } 42 | return s 43 | } 44 | 45 | func NewFirewallFromServerConfigFirewall(fc ServerConfigFirewall) (Firewall, error) { 46 | switch fc.Backend { 47 | case ServerConfigFirewallBackendIPTables: 48 | return newIPTablesFromServerConfigFirewall(fc) 49 | case ServerConfigFirewallBackendCommand: 50 | return newFirewallCommandFromServerConfigFirewall(fc) 51 | case ServerConfigFirewallBackendNone: 52 | return firewallDummy{}, nil 53 | } 54 | 55 | return nil, errors.New("unsupported firewall backend") 56 | } 57 | 58 | // firewallDummy does nothing, it is just used to satisfy the interface definition. It is mostly used 59 | // for testing/performance measurement purposes. Do not use for production work. 60 | type firewallDummy struct{} 61 | 62 | func (f firewallDummy) FirewallSetup() error { 63 | return nil 64 | } 65 | 66 | func (f firewallDummy) RuleAdd(r FirewallRule, meta FirewallRuleMetadata) error { 67 | return nil 68 | } 69 | 70 | func (f firewallDummy) RuleRemove(r FirewallRule, meta FirewallRuleMetadata) error { 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /internal/firewall_rule_manager.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "github.com/emirpasic/gods/lists" 9 | "github.com/emirpasic/gods/lists/doublylinkedlist" 10 | "github.com/greenstatic/openspa/internal/observability" 11 | "github.com/pkg/errors" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | type FirewallRuleManager struct { 16 | fw Firewall 17 | 18 | rules lists.List 19 | lock sync.Mutex 20 | 21 | stop chan struct{} 22 | metrics firewallRuleManagerMetrics 23 | } 24 | 25 | type firewallRuleManagerMetrics struct { 26 | rulesAdded observability.Counter 27 | rulesRemoved observability.Counter 28 | } 29 | 30 | func NewFirewallRuleManager(fw Firewall) *FirewallRuleManager { 31 | r := &FirewallRuleManager{ 32 | fw: fw, 33 | rules: doublylinkedlist.New(), 34 | metrics: newFirewallRuleManagerMetrics(), 35 | } 36 | return r 37 | } 38 | 39 | func (frm *FirewallRuleManager) Start() error { 40 | frm.stop = make(chan struct{}) 41 | go frm.cleanupRoutine(frm.stop) 42 | return nil 43 | } 44 | 45 | func (frm *FirewallRuleManager) cleanupRoutine(stop chan struct{}) { 46 | t := time.NewTicker(time.Second) 47 | for { 48 | select { 49 | case <-t.C: 50 | if err := frm.cleanup(); err != nil { 51 | log.Error().Err(err).Msgf("Firewall Rule Manager failed to cleanup") 52 | } 53 | case <-stop: 54 | t.Stop() 55 | return 56 | } 57 | } 58 | } 59 | 60 | func (frm *FirewallRuleManager) cleanup() error { 61 | frm.lock.Lock() 62 | defer frm.lock.Unlock() 63 | 64 | last := 0 65 | mainloop: 66 | for { 67 | size := frm.rules.Size() 68 | if size == 0 { 69 | break 70 | } 71 | for i, elm := range frm.rules.Values() { 72 | if i < last { 73 | continue 74 | } 75 | 76 | re, ok := elm.(FirewallRuleWithExpiration) 77 | if !ok { 78 | panic("invalid type in rule manger list") 79 | } 80 | 81 | if time.Now().After(re.Expiration()) { 82 | // remove 83 | err := frm.fw.RuleRemove(re.Rule, re.Meta) 84 | if err != nil { 85 | return errors.Wrap(err, "firewall rule remove") 86 | } 87 | frm.metrics.rulesRemoved.Inc() 88 | 89 | frm.rules.Remove(i) 90 | last = i 91 | break 92 | } 93 | 94 | if i+1 == size { 95 | break mainloop 96 | } 97 | } 98 | } 99 | 100 | return nil 101 | } 102 | 103 | func (frm *FirewallRuleManager) removeAllRules() []error { 104 | frm.lock.Lock() 105 | defer frm.lock.Unlock() 106 | 107 | errs := make([]error, 0) 108 | 109 | for _, elm := range frm.rules.Values() { 110 | re, ok := elm.(FirewallRuleWithExpiration) 111 | if !ok { 112 | panic("invalid type in rule manger list") 113 | } 114 | 115 | err := frm.fw.RuleRemove(re.Rule, re.Meta) 116 | if err != nil { 117 | errs = append(errs, errors.Wrap(err, fmt.Sprintf("firewall rule: %s", re.String()))) 118 | } 119 | } 120 | 121 | frm.rules.Clear() 122 | 123 | return errs 124 | } 125 | 126 | func (frm *FirewallRuleManager) Stop() error { 127 | frm.stop <- struct{}{} 128 | errs := frm.removeAllRules() 129 | if len(errs) != 0 { 130 | for _, err := range errs { 131 | log.Error().Msgf(err.Error()) 132 | } 133 | } 134 | return nil 135 | } 136 | 137 | func (frm *FirewallRuleManager) Add(r FirewallRule, meta FirewallRuleMetadata) error { 138 | re := FirewallRuleWithExpiration{ 139 | Rule: r, 140 | Meta: meta, 141 | Duration: meta.Duration, 142 | Created: time.Now(), 143 | } 144 | 145 | err := frm.fw.RuleAdd(r, meta) 146 | if err != nil { 147 | return errors.Wrap(err, "firewall rule add") 148 | } 149 | 150 | frm.metrics.rulesAdded.Inc() 151 | 152 | frm.lock.Lock() 153 | frm.rules.Add(re) 154 | frm.lock.Unlock() 155 | 156 | return nil 157 | } 158 | 159 | func (frm *FirewallRuleManager) Count() int { 160 | frm.lock.Lock() 161 | defer frm.lock.Unlock() 162 | return frm.rules.Size() 163 | } 164 | 165 | func (frm *FirewallRuleManager) Debug() map[string]interface{} { 166 | frm.lock.Lock() 167 | defer frm.lock.Unlock() 168 | 169 | rules := make([]string, 0, frm.rules.Size()) 170 | for _, elm := range frm.rules.Values() { 171 | r, ok := elm.(FirewallRule) 172 | if !ok { 173 | panic("invalid type in rules list") 174 | } 175 | 176 | rules = append(rules, r.String()) 177 | } 178 | 179 | return map[string]interface{}{ 180 | "rules": rules, 181 | } 182 | } 183 | 184 | func newFirewallRuleManagerMetrics() firewallRuleManagerMetrics { 185 | f := firewallRuleManagerMetrics{} 186 | mr := getMetricsRepository() 187 | lbl := observability.NewLabels() 188 | 189 | f.rulesAdded = mr.Count("fw_rules_added", lbl) 190 | f.rulesRemoved = mr.Count("fw_rules_removed", lbl) 191 | 192 | return f 193 | } 194 | 195 | type FirewallRuleWithExpiration struct { 196 | Rule FirewallRule 197 | Meta FirewallRuleMetadata 198 | Duration time.Duration 199 | Created time.Time 200 | } 201 | 202 | func (re *FirewallRuleWithExpiration) String() string { 203 | return fmt.Sprintf("%s (expires in: %s)", re.Rule.String(), re.Duration.String()) 204 | } 205 | 206 | func (re *FirewallRuleWithExpiration) Expiration() time.Time { 207 | return re.Created.Add(re.Duration) 208 | } 209 | -------------------------------------------------------------------------------- /internal/fw_command.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | 7 | "github.com/pkg/errors" 8 | "github.com/rs/zerolog/log" 9 | ) 10 | 11 | var _ Firewall = &FirewallCommand{} 12 | 13 | type FirewallCommand struct { 14 | FirewallSetupCmd string 15 | RuleAddCmd string 16 | RuleRemoveCmd string 17 | 18 | exec CommandExecuter 19 | } 20 | 21 | type FirewallCommandRuleAddInput struct { 22 | ClientUUID string `json:"clientUUID"` 23 | IPIsIPv6 bool `json:"ipIsIPv6"` 24 | ClientIP net.IP `json:"clientIP"` 25 | TargetIP net.IP `json:"targetIP"` 26 | TargetProtocol string `json:"targetProtocol"` 27 | PortStart int `json:"portStart"` 28 | PortEnd int `json:"portEnd,omitempty"` 29 | Duration int `json:"duration"` 30 | } 31 | 32 | type FirewallCommandRuleRemoveInput struct { 33 | ClientUUID string `json:"clientUUID"` 34 | IPIsIPv6 bool `json:"ipIsIPv6"` 35 | ClientIP net.IP `json:"clientIP"` 36 | TargetIP net.IP `json:"targetIP"` 37 | TargetProtocol string `json:"targetProtocol"` 38 | PortStart int `json:"portStart"` 39 | PortEnd int `json:"portEnd,omitempty"` 40 | } 41 | 42 | func NewFirewallCommand(setupCmd, ruleAddCmd, ruleRemoveCmd string) *FirewallCommand { 43 | fc := &FirewallCommand{ 44 | FirewallSetupCmd: setupCmd, 45 | RuleAddCmd: ruleAddCmd, 46 | RuleRemoveCmd: ruleRemoveCmd, 47 | exec: &CommandExecute{}, 48 | } 49 | 50 | return fc 51 | } 52 | 53 | func (fc *FirewallCommand) FirewallSetup() error { 54 | if fc.FirewallSetupCmd == "" { 55 | return nil 56 | } 57 | 58 | _, err := fc.exec.Execute(fc.FirewallSetupCmd, nil) 59 | return err 60 | } 61 | 62 | func (fc *FirewallCommand) RuleAdd(r FirewallRule, meta FirewallRuleMetadata) error { 63 | input := FirewallCommandRuleAddInput{ 64 | ClientUUID: meta.ClientUUID, 65 | IPIsIPv6: isIPv6(r.SrcIP), 66 | ClientIP: r.SrcIP, 67 | TargetIP: r.DstIP, 68 | TargetProtocol: r.Proto, 69 | PortStart: r.DstPortStart, 70 | PortEnd: r.DstPortEnd, 71 | Duration: int(meta.Duration.Seconds()), 72 | } 73 | 74 | stdin, err := json.Marshal(input) 75 | if err != nil { 76 | return errors.Wrap(err, "json marshal input") 77 | } 78 | 79 | output, err := fc.exec.Execute(fc.RuleAddCmd, stdin) 80 | if err != nil { 81 | log.Warn().Msgf("Failed to add rule %s, external command output: %s", r.String(), output) 82 | return errors.Wrap(err, "execute rule add command") 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (fc *FirewallCommand) RuleRemove(r FirewallRule, meta FirewallRuleMetadata) error { 89 | input := FirewallCommandRuleRemoveInput{ 90 | ClientUUID: meta.ClientUUID, 91 | IPIsIPv6: isIPv6(r.SrcIP), 92 | ClientIP: r.SrcIP, 93 | TargetIP: r.DstIP, 94 | TargetProtocol: r.Proto, 95 | PortStart: r.DstPortStart, 96 | PortEnd: r.DstPortEnd, 97 | } 98 | 99 | stdin, err := json.Marshal(input) 100 | if err != nil { 101 | return errors.Wrap(err, "json marshal input") 102 | } 103 | 104 | output, err := fc.exec.Execute(fc.RuleRemoveCmd, stdin) 105 | if err != nil { 106 | log.Warn().Msgf("Failed to remove rule %s, external command output: %s", r.String(), output) 107 | return errors.Wrap(err, "execute rule remove command") 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func newFirewallCommandFromServerConfigFirewall(fc ServerConfigFirewall) (*FirewallCommand, error) { 114 | setup := fc.Command.FirewallSetup 115 | add := fc.Command.RuleAdd 116 | remove := fc.Command.RuleRemove 117 | 118 | if len(add) == 0 { 119 | return nil, errors.New("rule add command is empty") 120 | } 121 | 122 | if len(remove) == 0 { 123 | return nil, errors.New("rule remove command is empty") 124 | } 125 | 126 | return NewFirewallCommand(setup, add, remove), nil 127 | } 128 | -------------------------------------------------------------------------------- /internal/fw_command_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/greenstatic/openspa/pkg/openspalib" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFirewallCommand_FirewallSetup(t *testing.T) { 14 | fc := NewFirewallCommand("setup-cmd", "rule-add", "rule-remove") 15 | exec := &CommandExecuteMock{} 16 | fc.exec = exec 17 | 18 | exec.On("Execute", "setup-cmd", []byte(nil), []string(nil)).Return([]byte{}, nil).Once() 19 | assert.NoError(t, fc.FirewallSetup()) 20 | 21 | exec.AssertExpectations(t) 22 | } 23 | func TestFirewallCommand_FirewallSetup_Empty(t *testing.T) { 24 | fc := NewFirewallCommand("", "rule-add", "rule-remove") 25 | exec := &CommandExecuteMock{} 26 | fc.exec = exec 27 | 28 | assert.NoError(t, fc.FirewallSetup()) 29 | 30 | exec.AssertExpectations(t) 31 | exec.AssertNumberOfCalls(t, "Execute", 0) 32 | } 33 | 34 | func TestFirewallCommand_RuleAdd(t *testing.T) { 35 | fc := NewFirewallCommand("setup-cmd", "rule-add", "rule-remove") 36 | exec := &CommandExecuteMock{} 37 | fc.exec = exec 38 | 39 | uuid := openspalib.RandomUUID() 40 | 41 | input := FirewallCommandRuleAddInput{ 42 | ClientUUID: uuid, 43 | IPIsIPv6: false, 44 | ClientIP: net.IPv4(88, 200, 12, 32), 45 | TargetIP: net.IPv4(88, 200, 98, 23), 46 | TargetProtocol: FirewallProtoTCP, 47 | PortStart: 80, 48 | PortEnd: 1000, 49 | Duration: 60 * 60, // 1 Hour 50 | } 51 | 52 | stdin, err := json.Marshal(input) 53 | assert.NoError(t, err) 54 | 55 | exec.On("Execute", "rule-add", stdin, []string(nil)).Return([]byte(nil), nil).Once() 56 | assert.NoError(t, fc.RuleAdd(FirewallRule{ 57 | Proto: FirewallProtoTCP, 58 | SrcIP: net.IPv4(88, 200, 12, 32), 59 | DstIP: net.IPv4(88, 200, 98, 23), 60 | DstPortStart: 80, 61 | DstPortEnd: 1000, 62 | }, FirewallRuleMetadata{ 63 | ClientUUID: uuid, 64 | Duration: time.Hour, 65 | })) 66 | 67 | exec.AssertExpectations(t) 68 | } 69 | 70 | func TestFirewallCommand_RuleRemove(t *testing.T) { 71 | fc := NewFirewallCommand("setup-cmd", "rule-add", "rule-remove") 72 | exec := &CommandExecuteMock{} 73 | fc.exec = exec 74 | 75 | uuid := openspalib.RandomUUID() 76 | 77 | input := FirewallCommandRuleRemoveInput{ 78 | ClientUUID: uuid, 79 | IPIsIPv6: false, 80 | ClientIP: net.IPv4(88, 200, 12, 32), 81 | TargetIP: net.IPv4(88, 200, 98, 23), 82 | TargetProtocol: FirewallProtoTCP, 83 | PortStart: 80, 84 | PortEnd: 1000, 85 | } 86 | 87 | stdin, err := json.Marshal(input) 88 | assert.NoError(t, err) 89 | 90 | exec.On("Execute", "rule-remove", stdin, []string(nil)).Return([]byte(nil), nil).Once() 91 | assert.NoError(t, fc.RuleRemove(FirewallRule{ 92 | Proto: FirewallProtoTCP, 93 | SrcIP: net.IPv4(88, 200, 12, 32), 94 | DstIP: net.IPv4(88, 200, 98, 23), 95 | DstPortStart: 80, 96 | DstPortEnd: 1000, 97 | }, FirewallRuleMetadata{ 98 | ClientUUID: uuid, 99 | Duration: time.Hour, 100 | })) 101 | 102 | exec.AssertExpectations(t) 103 | } 104 | -------------------------------------------------------------------------------- /internal/ip.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | const ( 14 | IPv4ServerDefault = "https://ipv4.openspa.org" 15 | IPv6ServerDefault = "https://ipv6.openspa.org" 16 | ) 17 | 18 | func GetIP(ipv4Server, ipv6Server string) { 19 | v4 := &PublicIPResolver{ 20 | ServerURL: ipv4Server, 21 | } 22 | v6 := &PublicIPResolver{ 23 | ServerURL: ipv6Server, 24 | } 25 | 26 | s := getIP(v4, v6) 27 | fmt.Print(s) 28 | } 29 | 30 | type IPResolver interface { 31 | GetPublicIP() (net.IP, error) 32 | } 33 | 34 | func getIP(ipv4, ipv6 IPResolver) string { 35 | s := strings.Builder{} 36 | s.WriteString("Public IPv4: ") 37 | 38 | v4, err := ipv4.GetPublicIP() 39 | if err != nil { 40 | s.WriteString("\n") 41 | s.WriteString("Error: ") 42 | s.WriteString(err.Error()) 43 | s.WriteString("\n") 44 | } else { 45 | s.WriteString(v4.String()) 46 | } 47 | s.WriteString("\n") 48 | 49 | s.WriteString("Public IPv6: ") 50 | 51 | v6, err := ipv6.GetPublicIP() 52 | if err != nil { 53 | s.WriteString("\n") 54 | s.WriteString("Error: ") 55 | s.WriteString(err.Error()) 56 | s.WriteString("\n") 57 | } else { 58 | s.WriteString(v6.String()) 59 | } 60 | s.WriteString("\n") 61 | 62 | return s.String() 63 | } 64 | 65 | var _ IPResolver = &PublicIPResolver{} 66 | 67 | type PublicIPResolver struct { 68 | ServerURL string 69 | } 70 | 71 | type publicIPResolverResponseBody struct { 72 | IP string `json:"IP"` 73 | } 74 | 75 | func (r *PublicIPResolver) GetPublicIP() (net.IP, error) { 76 | if r.ServerURL == "" { 77 | return nil, errors.New("invalid server url") 78 | } 79 | 80 | resp, err := http.Get(r.ServerURL) 81 | if err != nil { 82 | return nil, errors.Wrap(err, "get request failed") 83 | } 84 | 85 | defer resp.Body.Close() 86 | 87 | b := publicIPResolverResponseBody{} 88 | 89 | d := json.NewDecoder(resp.Body) 90 | if err := d.Decode(&b); err != nil { 91 | return nil, errors.Wrap(err, "response decode") 92 | } 93 | 94 | ip := net.ParseIP(b.IP) 95 | if ip == nil { 96 | return nil, errors.New("ip parse") 97 | } 98 | 99 | return ip, nil 100 | } 101 | -------------------------------------------------------------------------------- /internal/mocks_and_stubs.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | var _ UDPDatagramRequestHandler = &DatagramRequestHandlerMock{} 11 | 12 | type DatagramRequestHandlerMock struct { 13 | mock.Mock 14 | } 15 | 16 | func (d *DatagramRequestHandlerMock) DatagramRequestHandler(ctx context.Context, resp UDPResponser, r DatagramRequest) { 17 | d.Called(resp, r) 18 | } 19 | 20 | func (d *DatagramRequestHandlerMock) ADKSupport() bool { 21 | args := d.Called() 22 | return args.Bool(0) 23 | } 24 | 25 | func NewDatagramRequestHandlerMock() *DatagramRequestHandlerMock { 26 | d := &DatagramRequestHandlerMock{} 27 | return d 28 | } 29 | 30 | var _ UDPDatagramRequestHandler = &DatagramRequestHandlerStub{} 31 | 32 | type DatagramRequestHandlerStub struct { 33 | f func(ctx context.Context, resp UDPResponser, r DatagramRequest) 34 | adkSupport bool 35 | } 36 | 37 | func (d DatagramRequestHandlerStub) DatagramRequestHandler(ctx context.Context, resp UDPResponser, r DatagramRequest) { 38 | d.f(ctx, resp, r) 39 | } 40 | 41 | func (d DatagramRequestHandlerStub) ADKSupport() bool { 42 | return d.adkSupport 43 | } 44 | 45 | //nolint:lll 46 | func NewDatagramRequestHandlerStub(f func(ctx context.Context, resp UDPResponser, r DatagramRequest), adkSupport bool) *DatagramRequestHandlerStub { 47 | d := &DatagramRequestHandlerStub{ 48 | f: f, 49 | adkSupport: adkSupport, 50 | } 51 | return d 52 | } 53 | 54 | var _ UDPResponser = &UDPResponseMock{} 55 | 56 | type UDPResponseMock struct { 57 | mock.Mock 58 | } 59 | 60 | func (u *UDPResponseMock) SendUDPResponse(dst net.UDPAddr, body []byte) error { 61 | args := u.Called(dst, body) 62 | return args.Error(0) 63 | } 64 | 65 | var _ Firewall = &FirewallMock{} 66 | 67 | type FirewallMock struct { 68 | mock.Mock 69 | } 70 | 71 | func (fw *FirewallMock) RuleAdd(r FirewallRule, meta FirewallRuleMetadata) error { 72 | args := fw.Called(r, meta) 73 | return args.Error(0) 74 | } 75 | 76 | func (fw *FirewallMock) RuleRemove(r FirewallRule, meta FirewallRuleMetadata) error { 77 | args := fw.Called(r, meta) 78 | return args.Error(0) 79 | } 80 | 81 | func (fw *FirewallMock) FirewallSetup() error { 82 | args := fw.Called() 83 | return args.Error(0) 84 | } 85 | 86 | var _ Firewall = FirewallStub{} 87 | 88 | type FirewallStub struct{} 89 | 90 | func (FirewallStub) RuleAdd(r FirewallRule, meta FirewallRuleMetadata) error { 91 | return nil 92 | } 93 | 94 | func (FirewallStub) RuleRemove(r FirewallRule, meta FirewallRuleMetadata) error { 95 | return nil 96 | } 97 | 98 | func (FirewallStub) FirewallSetup() error { 99 | return nil 100 | } 101 | 102 | var _ CommandExecuter = &CommandExecuteMock{} 103 | 104 | type CommandExecuteMock struct { 105 | mock.Mock 106 | } 107 | 108 | func (c *CommandExecuteMock) Execute(cmd string, stdin []byte, args ...string) ([]byte, error) { 109 | a := c.Called(cmd, stdin, args) 110 | return a.Get(0).([]byte), a.Error(1) 111 | } 112 | -------------------------------------------------------------------------------- /internal/observability.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "github.com/greenstatic/openspa/internal/observability" 5 | "github.com/greenstatic/openspa/internal/observability/metrics" 6 | ) 7 | 8 | var _metrics = observability.GetGlobalMetricsRepository() 9 | 10 | var prometheusRepo *metrics.PrometheusRepository 11 | 12 | //nolint:gochecknoinits 13 | func init() { 14 | observability.OnMetricsRepositoryGlobalSet(SetMetricsRepository) 15 | 16 | prometheusRepo = metrics.NewPrometheusRepository(true) 17 | observability.SetGlobalMetricsRepository(prometheusRepo) 18 | } 19 | 20 | func getMetricsRepository() observability.MetricsRepository { 21 | return _metrics 22 | } 23 | 24 | func SetMetricsRepository(m observability.MetricsRepository) { 25 | _metrics = m 26 | } 27 | -------------------------------------------------------------------------------- /internal/observability/metrics.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | type MetricsRepository interface { 4 | CountRegistry 5 | GaugeRegistry 6 | } 7 | 8 | type Labels map[string]string 9 | 10 | // CountRegistry implements various Counter metrics. When choosing the Counter type, keep in mind that each Counter type 11 | // has its pros and cons. Check the implementation's documentation for details. 12 | type CountRegistry interface { 13 | Count(name string, l Labels) Counter 14 | 15 | // CountVec doesn't need constant labels (i.e. predefined label key(s) and value(s)), just constant label keys 16 | CountVec(name string, labelKeys ...string) CounterVec 17 | 18 | CountFunc(name string, l Labels) CounterFunc 19 | } 20 | 21 | type Counter interface { 22 | Inc() 23 | Add(count int) 24 | Get() int 25 | } 26 | 27 | type CounterVec interface { 28 | Inc(labelValues ...string) 29 | Add(count int, labelValues ...string) 30 | } 31 | 32 | type CounterFunc interface { 33 | CounterFuncRegister(fn func() float64) 34 | CounterFuncDeregister() 35 | } 36 | 37 | type GaugeRegistry interface { 38 | Gauge(name string, l Labels) Gauge 39 | 40 | GaugeFunc(name string, l Labels) GaugeFunc 41 | } 42 | 43 | type Gauge interface { 44 | Set(f float64) 45 | } 46 | 47 | type GaugeFunc interface { 48 | GaugeFuncRegister(fn func() float64) 49 | GaugeFuncDeregister() 50 | } 51 | 52 | func NewLabels() Labels { 53 | return make(map[string]string) 54 | } 55 | 56 | func (l Labels) ToMap() map[string]string { 57 | return l 58 | } 59 | 60 | // Add returns a new copy of Labels with an additional entry, the key/value this function is called with. 61 | func (l Labels) Add(key, value string) Labels { 62 | m := make(map[string]string) 63 | 64 | for k, v := range l { 65 | m[k] = v 66 | } 67 | 68 | m[key] = value 69 | return m 70 | } 71 | -------------------------------------------------------------------------------- /internal/observability/metrics_global.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var ( 8 | global = NewGlobalMetricsRepository(MetricsRepositoryStub{}) 9 | ) 10 | 11 | func GetGlobalMetricsRepository() MetricsRepository { 12 | return global.r 13 | } 14 | 15 | func SetGlobalMetricsRepository(mr MetricsRepository) { 16 | global.lock.Lock() 17 | defer global.lock.Unlock() 18 | 19 | global.r = mr 20 | for _, f := range global.changeCallbacks { 21 | f(mr) 22 | } 23 | } 24 | 25 | func OnMetricsRepositoryGlobalSet(f globalMetricsRepositoryChangeCallback) { 26 | global.lock.Lock() 27 | defer global.lock.Unlock() 28 | global.changeCallbacks = append(global.changeCallbacks, f) 29 | } 30 | 31 | type globalMetricsRepository struct { 32 | r MetricsRepository 33 | 34 | changeCallbacks []globalMetricsRepositoryChangeCallback 35 | lock sync.Mutex 36 | } 37 | 38 | type globalMetricsRepositoryChangeCallback func(m MetricsRepository) 39 | 40 | //nolint:revive 41 | func NewGlobalMetricsRepository(mr MetricsRepository) *globalMetricsRepository { 42 | g := &globalMetricsRepository{ 43 | r: mr, 44 | } 45 | g.changeCallbacks = make([]globalMetricsRepositoryChangeCallback, 0) 46 | return g 47 | } 48 | -------------------------------------------------------------------------------- /internal/observability/metrics_global_test.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestSetGlobalMetricsRepository(t *testing.T) { 10 | mr1 := GetGlobalMetricsRepository() 11 | 12 | type MetricsRepositoryStub2 struct { 13 | MetricsRepositoryStub 14 | } 15 | 16 | mr2 := MetricsRepositoryStub2{} 17 | 18 | SetGlobalMetricsRepository(mr2) 19 | 20 | assert.NotEqual(t, mr1, GetGlobalMetricsRepository()) 21 | assert.Equal(t, mr2, GetGlobalMetricsRepository()) 22 | } 23 | 24 | func TestOnMetricsRepositoryGlobalSet(t *testing.T) { 25 | calls := 0 26 | var mrCallback MetricsRepository 27 | callback := func(mr MetricsRepository) { 28 | calls++ 29 | mrCallback = mr 30 | } 31 | 32 | OnMetricsRepositoryGlobalSet(callback) 33 | assert.Equal(t, 0, calls) 34 | assert.Nil(t, mrCallback) 35 | 36 | type MetricsRepositoryStub2 struct { 37 | MetricsRepositoryStub 38 | } 39 | 40 | mr2 := MetricsRepositoryStub2{} 41 | SetGlobalMetricsRepository(mr2) 42 | 43 | assert.Equal(t, 1, calls) 44 | assert.NotNil(t, mrCallback) 45 | assert.Equal(t, mr2, mrCallback) 46 | } 47 | -------------------------------------------------------------------------------- /internal/observability/metrics_stub.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | var _ MetricsRepository = MetricsRepositoryStub{} 4 | 5 | type MetricsRepositoryStub struct { 6 | CountRegistryStub 7 | GaugeRegistryStub 8 | } 9 | 10 | var _ CountRegistry = CountRegistryStub{} 11 | 12 | type CountRegistryStub struct{} 13 | 14 | func (c CountRegistryStub) Count(_ string, _ Labels) Counter { 15 | cs := CounterStub(0) 16 | return &cs 17 | } 18 | 19 | func (c CountRegistryStub) CountVec(_ string, _ ...string) CounterVec { 20 | return CounterVecStub{} 21 | } 22 | 23 | func (c CountRegistryStub) CountFunc(_ string, _ Labels) CounterFunc { 24 | return CounterFuncStub{} 25 | } 26 | 27 | type GaugeRegistryStub struct{} 28 | 29 | func (g GaugeRegistryStub) Gauge(_ string, _ Labels) Gauge { 30 | return GaugeStub{} 31 | } 32 | 33 | func (g GaugeRegistryStub) GaugeFunc(_ string, _ Labels) GaugeFunc { 34 | return GaugeFuncStub{} 35 | } 36 | 37 | type CounterStub int 38 | 39 | func (c *CounterStub) Inc() { 40 | *c++ 41 | } 42 | 43 | func (c *CounterStub) Add(i int) { 44 | *c += CounterStub(i) 45 | } 46 | 47 | func (c *CounterStub) Get() int { 48 | return int(*c) 49 | } 50 | 51 | type CounterVecStub struct{} 52 | 53 | func (c CounterVecStub) Inc(_ ...string) {} 54 | 55 | func (c CounterVecStub) Add(_ int, _ ...string) {} 56 | 57 | type CounterFuncStub struct{} 58 | 59 | func (c CounterFuncStub) CounterFuncRegister(_ func() float64) {} 60 | 61 | func (c CounterFuncStub) CounterFuncDeregister() {} 62 | 63 | type GaugeStub struct{} 64 | 65 | func (g GaugeStub) Set(_ float64) {} 66 | 67 | type GaugeFuncStub struct{} 68 | 69 | func (g GaugeFuncStub) GaugeFuncRegister(_ func() float64) {} 70 | 71 | func (g GaugeFuncStub) GaugeFuncDeregister() {} 72 | -------------------------------------------------------------------------------- /internal/observability/metrics_test.go: -------------------------------------------------------------------------------- 1 | package observability 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestLabelsToMap(t *testing.T) { 11 | l := map[string]string{"foo": "bar", "state": "zoo"} 12 | 13 | lbl := Labels(l) 14 | assert.Equal(t, l, lbl.ToMap()) 15 | } 16 | 17 | func TestLabelsAdd_ShouldNotModifyPrevious(t *testing.T) { 18 | l := NewLabels() 19 | 20 | l1 := l.Add("foo", "bar") 21 | l2 := l1.Add("state", "zoo") 22 | 23 | require.Len(t, l1.ToMap(), 1) 24 | assert.Equal(t, "bar", l1.ToMap()["foo"]) 25 | 26 | require.Len(t, l2.ToMap(), 2) 27 | assert.Equal(t, "bar", l2.ToMap()["foo"]) 28 | assert.Equal(t, "zoo", l2.ToMap()["state"]) 29 | } 30 | -------------------------------------------------------------------------------- /internal/ospa.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | lib "github.com/greenstatic/openspa/pkg/openspalib" 8 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 9 | "github.com/pkg/errors" 10 | "github.com/rs/zerolog/log" 11 | uuid "github.com/satori/go.uuid" 12 | "gopkg.in/yaml.v2" 13 | ) 14 | 15 | const OSPAFileVersion = "0.2" 16 | 17 | type OSPA struct { 18 | Version string `yaml:"version"` 19 | ClientUUID string `yaml:"clientUUID"` 20 | ServerHost string `yaml:"serverHost"` 21 | ServerPort int `yaml:"serverPort"` 22 | ADK OSPAADK `yaml:"adk"` 23 | Crypto OSPACrypto `yaml:"crypto"` 24 | } 25 | 26 | type OSPAADK struct { 27 | Secret string `yaml:"secret"` 28 | } 29 | 30 | type OSPACrypto struct { 31 | CipherSuitePriority []string `yaml:"cipherSuitePriority"` 32 | RSA OSPACryptoRSA `yaml:"rsa"` 33 | } 34 | 35 | type OSPACryptoRSA struct { 36 | Client OSPACryptoRSAClient `yaml:"client"` 37 | Server OSPACryptoRSAServer `yaml:"server"` 38 | } 39 | 40 | type OSPACryptoRSAClient struct { 41 | PrivateKey string `yaml:"privateKey"` 42 | PublicKey string `yaml:"publicKey"` 43 | } 44 | 45 | type OSPACryptoRSAServer struct { 46 | PublicKey string `yaml:"publicKey"` 47 | } 48 | 49 | func OSPAFromFile(path string) (OSPA, error) { 50 | log.Debug().Msgf("Reading OSPA file: %s", path) 51 | return ospaFromFile(path) 52 | } 53 | 54 | func ospaFromFile(path string) (OSPA, error) { 55 | b, err := os.ReadFile(path) 56 | if err != nil { 57 | return OSPA{}, errors.Wrap(err, "file read") 58 | } 59 | 60 | return OSPAParse(b) 61 | } 62 | 63 | func OSPAParse(b []byte) (OSPA, error) { 64 | o := OSPA{} 65 | if err := yaml.Unmarshal(b, &o); err != nil { 66 | return OSPA{}, errors.Wrap(err, "yaml unmarshal") 67 | } 68 | return o, nil 69 | } 70 | 71 | func (o OSPA) Verify() error { 72 | if o.Version != OSPAFileVersion { 73 | return errors.New("unsupported file version") 74 | } 75 | 76 | _, err := uuid.FromString(o.ClientUUID) 77 | if err != nil { 78 | return errors.New("clientUUID invalid UUID") 79 | } 80 | 81 | if len(o.ServerHost) == 0 { 82 | return errors.New("server host invalid") 83 | } 84 | 85 | if !(o.ServerPort > 0 && o.ServerPort < 65535) { 86 | return errors.New("server port invalid") 87 | } 88 | 89 | if err := o.ADK.Verify(); err != nil { 90 | return errors.Wrap(err, "adk") 91 | } 92 | 93 | if err := o.Crypto.Verify(); err != nil { 94 | return errors.Wrap(err, "crypto") 95 | } 96 | 97 | return nil 98 | } 99 | 100 | func (o OSPAADK) Verify() error { 101 | if len(o.Secret) > 0 { 102 | if len(o.Secret) != lib.ADKSecretEncodedLen { 103 | return errors.New("secret length invalid") 104 | } 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (o OSPACrypto) Verify() error { 111 | if len(o.CipherSuitePriority) == 0 { 112 | return errors.New("cipherSuitePriority empty") 113 | } 114 | 115 | for _, cs := range o.CipherSuitePriority { 116 | if crypto.CipherSuiteStringToID(cs) == crypto.CipherUnknown { 117 | return errors.New("cipherSuitePriority unsupported/unknown cipher: " + cs) 118 | } 119 | } 120 | 121 | if err := o.RSA.Verify(); err != nil { 122 | return errors.Wrap(err, "rsa") 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func (o OSPACryptoRSA) Verify() error { 129 | if err := o.Client.Verify(); err != nil { 130 | return errors.Wrap(err, "client") 131 | } 132 | if err := o.Server.Verify(); err != nil { 133 | return errors.Wrap(err, "server") 134 | } 135 | return nil 136 | } 137 | 138 | func (o OSPACryptoRSAClient) Verify() error { 139 | if !strings.Contains(o.PrivateKey, "PRIVATE KEY") { 140 | return errors.New("private key is not in PKCS #1 ASN.1 DER format") 141 | } 142 | 143 | if !strings.Contains(o.PublicKey, "PUBLIC KEY") { 144 | return errors.New("public key is not in PKIX ASN.1 DER format") 145 | } 146 | return nil 147 | } 148 | 149 | func (o OSPACryptoRSAServer) Verify() error { 150 | if !strings.Contains(o.PublicKey, "PUBLIC KEY") { 151 | return errors.New("public key is not in PKIX ASN.1 DER format") 152 | } 153 | return nil 154 | } 155 | -------------------------------------------------------------------------------- /internal/ospa_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestOSPA(t *testing.T) { 10 | content := ` 11 | version: "0.2" 12 | 13 | clientUUID: "c3b66a05-9098-4100-8141-be5695ada0e7" 14 | 15 | # OpenSPA server 16 | serverHost: "localhost" # can be domain or IP 17 | serverPort: 22211 18 | 19 | adk: 20 | secret: "7O4ZIRI" 21 | 22 | crypto: 23 | cipherSuitePriority: 24 | - "CipherSuite_RSA_SHA256_AES256CBC" 25 | 26 | rsa: 27 | client: 28 | privateKey: | 29 | -----BEGIN RSA PRIVATE KEY----- 30 | 31 | -----END RSA PRIVATE KEY----- 32 | publicKey: | 33 | -----BEGIN RSA PUBLIC KEY----- 34 | 35 | -----END RSA PUBLIC KEY----- 36 | server: 37 | publicKey: | 38 | -----BEGIN RSA PUBLIC KEY----- 39 | 40 | -----END RSA PUBLIC KEY----- 41 | ` 42 | o, err := OSPAParse([]byte(content)) 43 | assert.NoError(t, err) 44 | 45 | assert.Equal(t, "0.2", o.Version) 46 | assert.Equal(t, "c3b66a05-9098-4100-8141-be5695ada0e7", o.ClientUUID) 47 | assert.Equal(t, "localhost", o.ServerHost) 48 | assert.Equal(t, 22211, o.ServerPort) 49 | assert.Equal(t, "7O4ZIRI", o.ADK.Secret) 50 | assert.Equal(t, []string{"CipherSuite_RSA_SHA256_AES256CBC"}, o.Crypto.CipherSuitePriority) 51 | 52 | clientPrivKey := "-----BEGIN RSA PRIVATE KEY-----\n\n-----END RSA PRIVATE KEY-----\n" 53 | assert.Equal(t, clientPrivKey, o.Crypto.RSA.Client.PrivateKey) 54 | 55 | clientPubKey := "-----BEGIN RSA PUBLIC KEY-----\n\n-----END RSA PUBLIC KEY-----\n" 56 | assert.Equal(t, clientPubKey, o.Crypto.RSA.Client.PublicKey) 57 | 58 | serverPubKey := "-----BEGIN RSA PUBLIC KEY-----\n\n-----END RSA PUBLIC KEY-----\n" 59 | assert.Equal(t, serverPubKey, o.Crypto.RSA.Server.PublicKey) 60 | 61 | assert.NoError(t, o.Verify()) 62 | } 63 | -------------------------------------------------------------------------------- /internal/server.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | crypt "crypto" 5 | "crypto/rsa" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/greenstatic/openspa/pkg/openspalib" 11 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 12 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | func NewServerCipherSuite(c ServerConfigCrypto) (crypto.CipherSuite, error) { 17 | privKey, err := rsaPrivateKeyFromFile(c.RSA.Server.PrivateKeyPath) 18 | if err != nil { 19 | return nil, errors.Wrap(err, "private key read") 20 | } 21 | 22 | l := NewPublicKeyLookupDir(c.RSA.Client.PublicKeyLookupDir) 23 | resolve := NewPublicKeyResolveFromClientUUID(l) 24 | 25 | cs := crypto.NewCipherSuite_RSA_SHA256_AES256CBC(privKey, resolve) 26 | return cs, nil 27 | } 28 | 29 | var _ crypto.PublicKeyResolver = PublicKeyResolveFromClientUUID{} 30 | 31 | type PublicKeyResolveFromClientUUID struct { 32 | l crypto.PublicKeyLookuper 33 | } 34 | 35 | func NewPublicKeyResolveFromClientUUID(l crypto.PublicKeyLookuper) *PublicKeyResolveFromClientUUID { 36 | p := &PublicKeyResolveFromClientUUID{ 37 | l: l, 38 | } 39 | return p 40 | } 41 | 42 | func (p PublicKeyResolveFromClientUUID) PublicKey(_, meta tlv.Container) (crypt.PublicKey, error) { 43 | if meta == nil { 44 | return nil, errors.New("no meta container") 45 | } 46 | 47 | uuid, err := openspalib.ClientUUIDFromContainer(meta) 48 | if err != nil { 49 | return nil, errors.Wrap(err, "client uuid from meta container") 50 | } 51 | 52 | pub, err := p.l.LookupPublicKey(uuid) 53 | if err != nil { 54 | return nil, errors.Wrap(err, "lookup public key") 55 | } 56 | 57 | return pub, nil 58 | } 59 | 60 | var _ crypto.PublicKeyLookuper = PublicKeyLookupDir{} 61 | 62 | type PublicKeyLookupDir struct { 63 | DirPath string 64 | } 65 | 66 | func NewPublicKeyLookupDir(dirPath string) *PublicKeyLookupDir { 67 | p := &PublicKeyLookupDir{ 68 | DirPath: dirPath, 69 | } 70 | return p 71 | } 72 | 73 | func (p PublicKeyLookupDir) LookupPublicKey(clientUUID string) (crypt.PublicKey, error) { 74 | de, err := os.ReadDir(p.DirPath) 75 | if err != nil { 76 | return nil, errors.Wrap(err, "read public key lookup dir") 77 | } 78 | 79 | for _, e := range de { 80 | if name := e.Name(); !e.IsDir() && p.clientFilenameMatch(clientUUID, name) { 81 | b, err := os.ReadFile(filepath.Join(p.DirPath, name)) 82 | if err != nil { 83 | return nil, errors.Wrap(err, "client key file read") 84 | } 85 | 86 | pub, err := crypto.RSADecodePublicKey(string(b)) 87 | if err != nil { 88 | return nil, errors.Wrap(err, "rsa decode client key") 89 | } 90 | 91 | return pub, nil 92 | } 93 | } 94 | 95 | return nil, errors.New("no key found") 96 | } 97 | 98 | func (p PublicKeyLookupDir) clientFilenameMatch(clientUUID, filename string) bool { 99 | if clientUUID == filename { 100 | return true 101 | } 102 | 103 | fx := strings.Split(filename, ".") 104 | if len(fx) > 1 { 105 | fx = fx[:len(fx)-1] 106 | } 107 | 108 | return strings.Join(fx, ".") == clientUUID 109 | } 110 | 111 | func rsaPrivateKeyFromFile(privateKeyPath string) (*rsa.PrivateKey, error) { 112 | content, err := os.ReadFile(privateKeyPath) 113 | if err != nil { 114 | return nil, errors.Wrap(err, "read file") 115 | } 116 | 117 | key, err := crypto.RSADecodePrivateKey(string(content)) 118 | if err != nil { 119 | return nil, errors.Wrap(err, "rsa decode private key") 120 | } 121 | 122 | return key, nil 123 | } 124 | 125 | //nolint:unused 126 | func rsaPublicKeyFromFile(publicKeyPath string) (*rsa.PublicKey, error) { 127 | content, err := os.ReadFile(publicKeyPath) 128 | if err != nil { 129 | return nil, errors.Wrap(err, "read file") 130 | } 131 | 132 | key, err := crypto.RSADecodePublicKey(string(content)) 133 | if err != nil { 134 | return nil, errors.Wrap(err, "rsa decode public key") 135 | } 136 | 137 | return key, nil 138 | } 139 | -------------------------------------------------------------------------------- /internal/server_http.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/greenstatic/openspa/internal/observability/metrics" 12 | "github.com/rs/zerolog/log" 13 | ) 14 | 15 | const ServerHTTPPortDefault = 22212 16 | 17 | type HTTPServer struct { 18 | bindIP net.IP 19 | bindPort int 20 | 21 | server *http.Server 22 | prom *metrics.PrometheusRepository 23 | } 24 | 25 | func NewHTTPServer(ip net.IP, port int) *HTTPServer { 26 | h := &HTTPServer{ 27 | bindIP: ip, 28 | bindPort: port, 29 | } 30 | 31 | return h 32 | } 33 | 34 | func (h *HTTPServer) Start() error { 35 | return h.start() 36 | } 37 | 38 | func (h *HTTPServer) start() error { 39 | h.prom = prometheusRepo 40 | 41 | mux := http.NewServeMux() 42 | h.setHandles(mux) 43 | 44 | h.server = &http.Server{ 45 | Handler: mux, 46 | Addr: net.JoinHostPort(h.bindIP.String(), strconv.Itoa(h.bindPort)), 47 | ReadTimeout: 10 * time.Second, 48 | WriteTimeout: 10 * time.Second, 49 | } 50 | 51 | log.Info().Msgf("Starting HTTP server on: %s", h.server.Addr) 52 | if err := h.server.ListenAndServe(); err != nil { 53 | if err == http.ErrServerClosed { 54 | log.Info().Msgf("HTTP server closed") 55 | } else { 56 | return err 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | func (h *HTTPServer) Stop() error { 64 | return h.stop() 65 | } 66 | 67 | func (h *HTTPServer) stop() error { 68 | if h.server == nil { 69 | return nil 70 | } 71 | 72 | log.Debug().Msgf("Stopping HTTP server") 73 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 74 | defer cancel() 75 | 76 | return h.server.Shutdown(ctx) 77 | } 78 | 79 | func (h *HTTPServer) setHandles(m *http.ServeMux) { 80 | m.HandleFunc("/", handleEndpointRoot) 81 | if h.prom != nil { 82 | m.Handle("/metrics", h.prom.Handler()) 83 | } 84 | } 85 | 86 | func handleEndpointRoot(w http.ResponseWriter, r *http.Request) { 87 | setHTTPResponseHeaders(w) 88 | 89 | if r.URL.Path != "/" { 90 | handleStatusNotFound(w, r) 91 | return 92 | } 93 | 94 | panicOnErr(json.NewEncoder(w).Encode(struct { 95 | Msg string `json:"msg"` 96 | Version string `json:"version"` 97 | }{ 98 | Msg: "OpenSPA Server", 99 | Version: Version(), 100 | })) 101 | } 102 | 103 | func handleStatusNotFound(w http.ResponseWriter, _ *http.Request) { 104 | w.WriteHeader(http.StatusNotFound) 105 | panicOnErr(json.NewEncoder(w).Encode(struct { 106 | Error string `json:"error"` 107 | }{ 108 | Error: "not found", 109 | })) 110 | } 111 | 112 | func setHTTPResponseHeaders(w http.ResponseWriter) { 113 | w.Header().Set("Content-Type", "application/json") 114 | } 115 | 116 | func panicOnErr(err error) { 117 | if err != nil { 118 | panic(err) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/server_http_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestHTTPServer(t *testing.T) { 15 | localhost := net.IPv4(127, 0, 0, 1).To4() 16 | 17 | serverPort := 23881 // sufficiently high port that is probably not taken 18 | 19 | h := NewHTTPServer(localhost, serverPort) 20 | done := make(chan bool) 21 | go func() { 22 | err := h.Start() 23 | assert.NoError(t, err) 24 | done <- true 25 | }() 26 | 27 | time.Sleep(time.Second) // wait for the HTTP server to be ready 28 | 29 | c := http.DefaultClient 30 | c.Timeout = time.Second 31 | 32 | resp, err := c.Get(fmt.Sprintf("http://localhost:%d/", serverPort)) 33 | assert.NoError(t, err) 34 | assert.NotNil(t, resp) 35 | 36 | defer resp.Body.Close() 37 | 38 | respSpec := struct { 39 | Msg string `json:"msg"` 40 | Version string `json:"version"` 41 | }{} 42 | 43 | assert.NoError(t, json.NewDecoder(resp.Body).Decode(&respSpec)) 44 | 45 | assert.Equal(t, "OpenSPA Server", respSpec.Msg) 46 | assert.Equal(t, Version(), respSpec.Version) 47 | 48 | assert.NoError(t, h.Stop()) 49 | 50 | tm := time.NewTimer(5 * time.Second) 51 | select { 52 | case <-done: 53 | case <-tm.C: 54 | t.Error("Timeout") 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/server_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "io/fs" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/greenstatic/openspa/pkg/openspalib" 10 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 11 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestPublicKeyLookupDir_LookupPublicKey(t *testing.T) { 17 | dir, err := os.MkdirTemp("", "testPublicKeyLookupDir") 18 | require.NoError(t, err) 19 | 20 | defer func() { 21 | if err := os.RemoveAll(dir); err != nil { 22 | t.Error(err) 23 | } 24 | }() 25 | 26 | _, pub1, err := crypto.RSAKeypair(2048) 27 | require.NoError(t, err) 28 | 29 | _, pub2, err := crypto.RSAKeypair(2048) 30 | require.NoError(t, err) 31 | 32 | pub1Str, err := crypto.RSAEncodePublicKey(pub1) 33 | require.NoError(t, err) 34 | pub2Str, err := crypto.RSAEncodePublicKey(pub2) 35 | require.NoError(t, err) 36 | 37 | require.NoError(t, os.WriteFile(filepath.Join(dir, "client1.key"), []byte(pub1Str), fs.ModePerm)) 38 | require.NoError(t, os.WriteFile(filepath.Join(dir, "client2.key"), []byte(pub2Str), fs.ModePerm)) 39 | 40 | l := NewPublicKeyLookupDir(dir) 41 | 42 | pubKey, err := l.LookupPublicKey("client1") 43 | assert.NoError(t, err) 44 | assert.NotNil(t, pubKey) 45 | 46 | pubKey2, err := l.LookupPublicKey("client2.key") 47 | assert.NoError(t, err) 48 | assert.NotNil(t, pubKey2) 49 | 50 | pubKey3, err := l.LookupPublicKey("client3.key") 51 | assert.Error(t, err) 52 | assert.Nil(t, pubKey3) 53 | } 54 | 55 | func TestPublicKeyLookupDir_clientFilenameMatch(t *testing.T) { 56 | p := PublicKeyLookupDir{} 57 | 58 | assert.True(t, p.clientFilenameMatch("client1", "client1")) 59 | assert.True(t, p.clientFilenameMatch("client1", "client1.key")) 60 | assert.True(t, p.clientFilenameMatch("client1", "client1.pub")) 61 | assert.True(t, p.clientFilenameMatch("client1", "client1.foo")) 62 | assert.False(t, p.clientFilenameMatch("client1", "client1.foo.key")) 63 | assert.False(t, p.clientFilenameMatch("client1", "client")) 64 | assert.False(t, p.clientFilenameMatch("client1", "client2")) 65 | assert.False(t, p.clientFilenameMatch("client1", "")) 66 | assert.False(t, p.clientFilenameMatch("client1", "client11")) 67 | } 68 | 69 | func TestPublicKeyResolveFromClientUUID_PublicKey(t *testing.T) { 70 | l := crypto.NewPublicKeyLookupMock() 71 | p := NewPublicKeyResolveFromClientUUID(l) 72 | 73 | uuid := "2542286f-7bba-4965-a57d-83bcdd744afb" 74 | 75 | c := tlv.NewContainer() 76 | assert.NoError(t, openspalib.ClientUUIDToContainer(c, uuid)) 77 | 78 | _, pub, err := crypto.RSAKeypair(2048) 79 | require.NoError(t, err) 80 | 81 | l.On("LookupPublicKey", uuid).Return(pub, nil).Once() 82 | 83 | pub2, err := p.PublicKey(nil, c) 84 | assert.NoError(t, err) 85 | assert.NotNil(t, pub2) 86 | 87 | assert.Equal(t, pub, pub2) 88 | 89 | l.AssertExpectations(t) 90 | } 91 | 92 | func TestPublicKeyResolveFromClientUUID_PublicKey_NoPanic(t *testing.T) { 93 | l := crypto.NewPublicKeyLookupMock() 94 | p := NewPublicKeyResolveFromClientUUID(l) 95 | 96 | assert.NotPanics(t, func() { 97 | _, err := p.PublicKey(nil, nil) 98 | assert.Error(t, err) 99 | }) 100 | 101 | l.AssertExpectations(t) 102 | } 103 | -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "fmt" 4 | 5 | const VersionMajor = 0 6 | const VersionMinor = 0 7 | const VersionBugfix = 1 8 | const VersionInfo = "dev" 9 | 10 | func Version() string { 11 | base := fmt.Sprintf("%d.%d.%d", VersionMajor, VersionMinor, VersionBugfix) 12 | if VersionInfo != "" { 13 | base += "-" + VersionInfo 14 | } 15 | 16 | return base 17 | } 18 | -------------------------------------------------------------------------------- /internal/xdp/Makefile: -------------------------------------------------------------------------------- 1 | CLANG ?= clang-14 2 | STRIP ?= llvm-strip-14 3 | OBJCOPY ?= llvm-objcopy-14 4 | CFLAGS := -O2 -g -Wall -Wno-unused-value -Wno-pointer-sign -Wno-compare-distinct-pointer-types -Werror $(CFLAGS) 5 | 6 | # From: https://github.com/cilium/ebpf/blob/master/Makefile 7 | CONTAINER_ENGINE ?= docker 8 | CONTAINER_IMAGE ?= quay.io/cilium/ebpf-builder 9 | CONTAINER_VERSION ?= 1648566014 10 | 11 | SOURCE_DIR := $(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))) 12 | 13 | .PHONY: generate 14 | # $BPF_CLANG is used in go:generate invocations. 15 | generate: export BPF_CLANG := $(CLANG) 16 | generate: export BPF_CFLAGS := $(CFLAGS) 17 | generate: 18 | go generate -tags xdp ./ 19 | 20 | .PHONY: clean 21 | clean: 22 | -$(RM) *.o 23 | -$(RM) bpf_bpfeb.go bpf_bpfel.go 24 | 25 | .PHONY: build 26 | build: 27 | ${CONTAINER_ENGINE} run --rm \ 28 | -v "${SOURCE_DIR}":/ebpf -w /ebpf --env MAKEFLAGS \ 29 | --env CFLAGS="-fdebug-prefix-map=/ebpf=." \ 30 | --env HOME="/tmp" \ 31 | "${CONTAINER_IMAGE}:${CONTAINER_VERSION}" \ 32 | make build-in-container 33 | 34 | # Due to missing in the container, the workaround is to install gcc-multilib. The down side is that we 35 | # won't have an identical build environment each time due to the remote package dependency. 36 | .PHONY: build-in-container 37 | build-in-container: 38 | apt update 39 | apt install -y gcc-multilib 40 | make generate 41 | 42 | 43 | .PHONY: container-shell 44 | container-shell: 45 | ${CONTAINER_ENGINE} run --rm -ti \ 46 | -v "${SOURCE_DIR}":/ebpf -w /ebpf \ 47 | "${CONTAINER_IMAGE}:${CONTAINER_VERSION}" 48 | -------------------------------------------------------------------------------- /internal/xdp/README.md: -------------------------------------------------------------------------------- 1 | # XDP 2 | This package contains XDP/eBPF code to accelerate ADK. 3 | 4 | It is container in its own Go module because of the way the bpf ELF file is built. 5 | 6 | This repo already contains the built ELF artifacts (`bpf_bpfeb.o`, `bpf_bpfel.o`) and associated Go files 7 | (`bpf_bpfeb.go`, `bpf_bpfel.go`). 8 | In order to build them from scratch you can use the provided container image: 9 | 10 | ```sh 11 | # Docker is required 12 | $ make build 13 | ``` 14 | -------------------------------------------------------------------------------- /internal/xdp/adk_common.go: -------------------------------------------------------------------------------- 1 | package xdp 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/rs/zerolog/log" 7 | ) 8 | 9 | type ADK interface { 10 | StatsProvider 11 | Start() error 12 | Stop() error 13 | } 14 | 15 | type StatsProvider interface { 16 | Stats() (Stats, error) 17 | } 18 | 19 | type ADKProofGenerator interface { 20 | ADKProofNow() uint32 21 | ADKProofNext() uint32 22 | } 23 | 24 | const ADKProofLength = 4 // bytes 25 | 26 | type ADKSettings struct { 27 | InterfaceName string 28 | Mode Mode 29 | ReplaceIfLoaded bool 30 | UDPServerPort int 31 | } 32 | 33 | type Stats struct { 34 | XDPAborted StatsRecord 35 | XDPDrop StatsRecord 36 | XDPPass StatsRecord 37 | XDPTX StatsRecord 38 | XDPRedirect StatsRecord 39 | 40 | OpenSPANot uint64 41 | OpenSPAADKProofInvalid uint64 42 | OpenSPAADKProofValid uint64 43 | } 44 | 45 | type StatsRecord struct { 46 | Packets uint64 47 | Bytes uint64 48 | } 49 | 50 | type adkProofSetter interface { 51 | setADKProof(g ADKProofGenerator) error 52 | } 53 | 54 | type adkProofSynchronize struct { 55 | setter adkProofSetter 56 | generator ADKProofGenerator 57 | period time.Duration 58 | 59 | quit chan bool 60 | } 61 | 62 | func newADKProofSynchronize(s adkProofSetter, g ADKProofGenerator, period time.Duration) *adkProofSynchronize { 63 | a := &adkProofSynchronize{ 64 | setter: s, 65 | generator: g, 66 | period: period, 67 | } 68 | return a 69 | } 70 | 71 | func (a *adkProofSynchronize) Start() { 72 | if a.quit != nil { 73 | // Already running 74 | return 75 | } 76 | a.quit = make(chan bool) 77 | 78 | go a.routine(a.quit) 79 | } 80 | 81 | func (a *adkProofSynchronize) routine(stop chan bool) { 82 | t := time.NewTicker(a.period) 83 | 84 | a.routineEvent() 85 | for { 86 | select { 87 | case <-t.C: 88 | a.routineEvent() 89 | case <-stop: 90 | return 91 | } 92 | } 93 | } 94 | 95 | func (a *adkProofSynchronize) routineEvent() { 96 | err := a.setter.setADKProof(a.generator) 97 | if err != nil { 98 | log.Error().Err(err).Msgf("Failed to set ADK proof in XDP") 99 | } 100 | } 101 | 102 | func (a *adkProofSynchronize) Stop() { 103 | if a.quit == nil { 104 | // Not running 105 | return 106 | } 107 | 108 | a.quit <- true 109 | a.quit = nil 110 | } 111 | 112 | func (s Stats) Merge(u Stats) Stats { 113 | out := s 114 | 115 | if u.XDPAborted.Packets > 0 { 116 | out.XDPAborted.Packets = u.XDPAborted.Packets 117 | } 118 | 119 | if u.XDPAborted.Bytes > 0 { 120 | out.XDPAborted.Bytes = u.XDPAborted.Bytes 121 | } 122 | 123 | if u.XDPDrop.Packets > 0 { 124 | out.XDPDrop.Packets = u.XDPDrop.Packets 125 | } 126 | 127 | if u.XDPDrop.Bytes > 0 { 128 | out.XDPDrop.Bytes = u.XDPDrop.Bytes 129 | } 130 | 131 | if u.XDPPass.Packets > 0 { 132 | out.XDPPass.Packets = u.XDPPass.Packets 133 | } 134 | 135 | if u.XDPPass.Bytes > 0 { 136 | out.XDPPass.Bytes = u.XDPPass.Bytes 137 | } 138 | 139 | if u.XDPTX.Packets > 0 { 140 | out.XDPTX.Packets = u.XDPTX.Packets 141 | } 142 | 143 | if u.XDPTX.Bytes > 0 { 144 | out.XDPTX.Bytes = u.XDPTX.Bytes 145 | } 146 | 147 | if u.XDPRedirect.Packets > 0 { 148 | out.XDPRedirect.Packets = u.XDPRedirect.Packets 149 | } 150 | 151 | if u.XDPRedirect.Bytes > 0 { 152 | out.XDPRedirect.Bytes = u.XDPRedirect.Bytes 153 | } 154 | 155 | if u.OpenSPANot > 0 { 156 | out.OpenSPANot = u.OpenSPANot 157 | } 158 | 159 | if u.OpenSPAADKProofInvalid > 0 { 160 | out.OpenSPAADKProofInvalid = u.OpenSPAADKProofInvalid 161 | } 162 | 163 | if u.OpenSPAADKProofValid > 0 { 164 | out.OpenSPAADKProofValid = u.OpenSPAADKProofValid 165 | } 166 | 167 | return out 168 | } 169 | -------------------------------------------------------------------------------- /internal/xdp/adk_common_test.go: -------------------------------------------------------------------------------- 1 | package xdp 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | func TestADKProofSynchronize_NumberOfSetProofCalls(t *testing.T) { 11 | const iterations = 4 12 | const iterationDur = time.Second 13 | 14 | m := &adkProofSetterMock{} 15 | s := newADKProofSynchronize(m, nil, iterationDur) 16 | 17 | m.On("setADKProof", mock.Anything).Return(nil).Times(iterations) 18 | 19 | s.Start() 20 | 21 | buffer := iterationDur / 10 22 | dur := (iterations - 1) * iterationDur // the first call to setADKProof should be done immediately (without waiting) 23 | time.Sleep(dur + buffer) 24 | 25 | s.Stop() 26 | 27 | m.AssertExpectations(t) 28 | } 29 | 30 | func TestADKProofSynchronize_StartStop(t *testing.T) { 31 | const iterations = 4 32 | const iterationDur = time.Second 33 | 34 | m := &adkProofSetterMock{} 35 | s := newADKProofSynchronize(m, nil, iterationDur) 36 | 37 | m.On("setADKProof", mock.Anything).Return(nil).Times(iterations) 38 | 39 | for i := 0; i < iterations; i++ { 40 | s.Start() 41 | buffer := iterationDur / 10 42 | time.Sleep(buffer) 43 | s.Stop() 44 | } 45 | 46 | m.AssertExpectations(t) 47 | } 48 | 49 | type adkProofSetterMock struct { 50 | mock.Mock 51 | } 52 | 53 | func (a *adkProofSetterMock) setADKProof(g ADKProofGenerator) error { 54 | args := a.Called(g) 55 | return args.Error(0) 56 | } 57 | -------------------------------------------------------------------------------- /internal/xdp/adk_none.go: -------------------------------------------------------------------------------- 1 | //go:build !xdp 2 | 3 | //nolint:unused 4 | package xdp 5 | 6 | import "errors" 7 | 8 | func IsSupported() bool { 9 | return false 10 | } 11 | 12 | type adk struct{} 13 | 14 | var ErrNotSupported = errors.New("XDP support is not supported") 15 | 16 | func NewADK(s ADKSettings, proof ADKProofGenerator) (ADK, error) { 17 | return nil, ErrNotSupported 18 | } 19 | 20 | func (a adk) Start() error { 21 | return ErrNotSupported 22 | } 23 | 24 | func (a adk) Stop() error { 25 | return ErrNotSupported 26 | } 27 | 28 | func (a adk) Stats() (Stats, error) { 29 | return Stats{}, ErrNotSupported 30 | } 31 | -------------------------------------------------------------------------------- /internal/xdp/bpf_bpfeb.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 3 | // +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 4 | 5 | package xdp 6 | 7 | import ( 8 | "bytes" 9 | _ "embed" 10 | "fmt" 11 | "io" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type bpfOspaStatDatarec struct{ Value uint64 } 17 | 18 | type bpfStatsDatarec struct { 19 | RxPackets uint64 20 | RxBytes uint64 21 | } 22 | 23 | // loadBpf returns the embedded CollectionSpec for bpf. 24 | func loadBpf() (*ebpf.CollectionSpec, error) { 25 | reader := bytes.NewReader(_BpfBytes) 26 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 27 | if err != nil { 28 | return nil, fmt.Errorf("can't load bpf: %w", err) 29 | } 30 | 31 | return spec, err 32 | } 33 | 34 | // loadBpfObjects loads bpf and converts it into a struct. 35 | // 36 | // The following types are suitable as obj argument: 37 | // 38 | // *bpfObjects 39 | // *bpfPrograms 40 | // *bpfMaps 41 | // 42 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 43 | func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 44 | spec, err := loadBpf() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return spec.LoadAndAssign(obj, opts) 50 | } 51 | 52 | // bpfSpecs contains maps and programs before they are loaded into the kernel. 53 | // 54 | // It can be passed ebpf.CollectionSpec.Assign. 55 | type bpfSpecs struct { 56 | bpfProgramSpecs 57 | bpfMapSpecs 58 | } 59 | 60 | // bpfSpecs contains programs before they are loaded into the kernel. 61 | // 62 | // It can be passed ebpf.CollectionSpec.Assign. 63 | type bpfProgramSpecs struct { 64 | XdpOpenspaAdk *ebpf.ProgramSpec `ebpf:"xdp_openspa_adk"` 65 | } 66 | 67 | // bpfMapSpecs contains maps before they are loaded into the kernel. 68 | // 69 | // It can be passed ebpf.CollectionSpec.Assign. 70 | type bpfMapSpecs struct { 71 | XdpConfigMap *ebpf.MapSpec `ebpf:"xdp_config_map"` 72 | XdpOpenspaStatsMap *ebpf.MapSpec `ebpf:"xdp_openspa_stats_map"` 73 | XdpStatsMap *ebpf.MapSpec `ebpf:"xdp_stats_map"` 74 | } 75 | 76 | // bpfObjects contains all objects after they have been loaded into the kernel. 77 | // 78 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 79 | type bpfObjects struct { 80 | bpfPrograms 81 | bpfMaps 82 | } 83 | 84 | func (o *bpfObjects) Close() error { 85 | return _BpfClose( 86 | &o.bpfPrograms, 87 | &o.bpfMaps, 88 | ) 89 | } 90 | 91 | // bpfMaps contains all maps after they have been loaded into the kernel. 92 | // 93 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 94 | type bpfMaps struct { 95 | XdpConfigMap *ebpf.Map `ebpf:"xdp_config_map"` 96 | XdpOpenspaStatsMap *ebpf.Map `ebpf:"xdp_openspa_stats_map"` 97 | XdpStatsMap *ebpf.Map `ebpf:"xdp_stats_map"` 98 | } 99 | 100 | func (m *bpfMaps) Close() error { 101 | return _BpfClose( 102 | m.XdpConfigMap, 103 | m.XdpOpenspaStatsMap, 104 | m.XdpStatsMap, 105 | ) 106 | } 107 | 108 | // bpfPrograms contains all programs after they have been loaded into the kernel. 109 | // 110 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 111 | type bpfPrograms struct { 112 | XdpOpenspaAdk *ebpf.Program `ebpf:"xdp_openspa_adk"` 113 | } 114 | 115 | func (p *bpfPrograms) Close() error { 116 | return _BpfClose( 117 | p.XdpOpenspaAdk, 118 | ) 119 | } 120 | 121 | func _BpfClose(closers ...io.Closer) error { 122 | for _, closer := range closers { 123 | if err := closer.Close(); err != nil { 124 | return err 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | // Do not access this directly. 131 | //go:embed bpf_bpfeb.o 132 | var _BpfBytes []byte 133 | -------------------------------------------------------------------------------- /internal/xdp/bpf_bpfeb.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenstatic/openspa/edc748cfbcd34acb2865e24fdd1430c941bed0e5/internal/xdp/bpf_bpfeb.o -------------------------------------------------------------------------------- /internal/xdp/bpf_bpfel.go: -------------------------------------------------------------------------------- 1 | // Code generated by bpf2go; DO NOT EDIT. 2 | //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 3 | // +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 4 | 5 | package xdp 6 | 7 | import ( 8 | "bytes" 9 | _ "embed" 10 | "fmt" 11 | "io" 12 | 13 | "github.com/cilium/ebpf" 14 | ) 15 | 16 | type bpfOspaStatDatarec struct{ Value uint64 } 17 | 18 | type bpfStatsDatarec struct { 19 | RxPackets uint64 20 | RxBytes uint64 21 | } 22 | 23 | // loadBpf returns the embedded CollectionSpec for bpf. 24 | func loadBpf() (*ebpf.CollectionSpec, error) { 25 | reader := bytes.NewReader(_BpfBytes) 26 | spec, err := ebpf.LoadCollectionSpecFromReader(reader) 27 | if err != nil { 28 | return nil, fmt.Errorf("can't load bpf: %w", err) 29 | } 30 | 31 | return spec, err 32 | } 33 | 34 | // loadBpfObjects loads bpf and converts it into a struct. 35 | // 36 | // The following types are suitable as obj argument: 37 | // 38 | // *bpfObjects 39 | // *bpfPrograms 40 | // *bpfMaps 41 | // 42 | // See ebpf.CollectionSpec.LoadAndAssign documentation for details. 43 | func loadBpfObjects(obj interface{}, opts *ebpf.CollectionOptions) error { 44 | spec, err := loadBpf() 45 | if err != nil { 46 | return err 47 | } 48 | 49 | return spec.LoadAndAssign(obj, opts) 50 | } 51 | 52 | // bpfSpecs contains maps and programs before they are loaded into the kernel. 53 | // 54 | // It can be passed ebpf.CollectionSpec.Assign. 55 | type bpfSpecs struct { 56 | bpfProgramSpecs 57 | bpfMapSpecs 58 | } 59 | 60 | // bpfSpecs contains programs before they are loaded into the kernel. 61 | // 62 | // It can be passed ebpf.CollectionSpec.Assign. 63 | type bpfProgramSpecs struct { 64 | XdpOpenspaAdk *ebpf.ProgramSpec `ebpf:"xdp_openspa_adk"` 65 | } 66 | 67 | // bpfMapSpecs contains maps before they are loaded into the kernel. 68 | // 69 | // It can be passed ebpf.CollectionSpec.Assign. 70 | type bpfMapSpecs struct { 71 | XdpConfigMap *ebpf.MapSpec `ebpf:"xdp_config_map"` 72 | XdpOpenspaStatsMap *ebpf.MapSpec `ebpf:"xdp_openspa_stats_map"` 73 | XdpStatsMap *ebpf.MapSpec `ebpf:"xdp_stats_map"` 74 | } 75 | 76 | // bpfObjects contains all objects after they have been loaded into the kernel. 77 | // 78 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 79 | type bpfObjects struct { 80 | bpfPrograms 81 | bpfMaps 82 | } 83 | 84 | func (o *bpfObjects) Close() error { 85 | return _BpfClose( 86 | &o.bpfPrograms, 87 | &o.bpfMaps, 88 | ) 89 | } 90 | 91 | // bpfMaps contains all maps after they have been loaded into the kernel. 92 | // 93 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 94 | type bpfMaps struct { 95 | XdpConfigMap *ebpf.Map `ebpf:"xdp_config_map"` 96 | XdpOpenspaStatsMap *ebpf.Map `ebpf:"xdp_openspa_stats_map"` 97 | XdpStatsMap *ebpf.Map `ebpf:"xdp_stats_map"` 98 | } 99 | 100 | func (m *bpfMaps) Close() error { 101 | return _BpfClose( 102 | m.XdpConfigMap, 103 | m.XdpOpenspaStatsMap, 104 | m.XdpStatsMap, 105 | ) 106 | } 107 | 108 | // bpfPrograms contains all programs after they have been loaded into the kernel. 109 | // 110 | // It can be passed to loadBpfObjects or ebpf.CollectionSpec.LoadAndAssign. 111 | type bpfPrograms struct { 112 | XdpOpenspaAdk *ebpf.Program `ebpf:"xdp_openspa_adk"` 113 | } 114 | 115 | func (p *bpfPrograms) Close() error { 116 | return _BpfClose( 117 | p.XdpOpenspaAdk, 118 | ) 119 | } 120 | 121 | func _BpfClose(closers ...io.Closer) error { 122 | for _, closer := range closers { 123 | if err := closer.Close(); err != nil { 124 | return err 125 | } 126 | } 127 | return nil 128 | } 129 | 130 | // Do not access this directly. 131 | //go:embed bpf_bpfel.o 132 | var _BpfBytes []byte 133 | -------------------------------------------------------------------------------- /internal/xdp/bpf_bpfel.o: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/greenstatic/openspa/edc748cfbcd34acb2865e24fdd1430c941bed0e5/internal/xdp/bpf_bpfel.o -------------------------------------------------------------------------------- /internal/xdp/byteorder_eb.go: -------------------------------------------------------------------------------- 1 | //go:build arm64be || armbe || mips || mips64 || mips64p32 || ppc64 || s390 || s390x || sparc || sparc64 2 | // +build arm64be armbe mips mips64 mips64p32 ppc64 s390 s390x sparc sparc64 3 | 4 | package xdp 5 | 6 | import "encoding/binary" 7 | 8 | var NativeOrder = binary.BigEndian 9 | -------------------------------------------------------------------------------- /internal/xdp/byteorder_el.go: -------------------------------------------------------------------------------- 1 | //go:build 386 || amd64 || amd64p32 || arm || arm64 || mips64le || mips64p32le || mipsle || ppc64le || riscv64 2 | // +build 386 amd64 amd64p32 arm arm64 mips64le mips64p32le mipsle ppc64le riscv64 3 | 4 | package xdp 5 | 6 | import ( 7 | "encoding/binary" 8 | ) 9 | 10 | var NativeOrder = binary.LittleEndian 11 | -------------------------------------------------------------------------------- /internal/xdp/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/greenstatic/openspa/internal/xdp 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/cilium/ebpf v0.9.3 7 | github.com/pkg/errors v0.9.1 8 | github.com/rs/zerolog v1.28.0 9 | github.com/stretchr/testify v1.8.0 10 | github.com/vishvananda/netlink v1.1.0 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/mattn/go-colorable v0.1.12 // indirect 16 | github.com/mattn/go-isatty v0.0.14 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/stretchr/objx v0.4.0 // indirect 19 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df // indirect 20 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /internal/xdp/go.sum: -------------------------------------------------------------------------------- 1 | github.com/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc= 2 | github.com/cilium/ebpf v0.9.3/go.mod h1:w27N4UjpaQ9X/DGrSugxUG+H+NhgntDuPb5lCzxCn8A= 3 | github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss= 8 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 9 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 10 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 12 | github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= 13 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 14 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 15 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 16 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 17 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 18 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 19 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 20 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= 21 | github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 22 | github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= 23 | github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= 24 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 25 | github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= 26 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 27 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 28 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 29 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 30 | github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0= 31 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 32 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k= 33 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 34 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 36 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= 38 | golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 40 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 41 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 42 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 43 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 44 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 45 | -------------------------------------------------------------------------------- /internal/xdp/headers/LICENSE.BSD-2-Clause: -------------------------------------------------------------------------------- 1 | Valid-License-Identifier: BSD-2-Clause 2 | SPDX-URL: https://spdx.org/licenses/BSD-2-Clause.html 3 | Usage-Guide: 4 | To use the BSD 2-clause "Simplified" License put the following SPDX 5 | tag/value pair into a comment according to the placement guidelines in 6 | the licensing rules documentation: 7 | SPDX-License-Identifier: BSD-2-Clause 8 | License-Text: 9 | 10 | Copyright (c) 2015 The Libbpf Authors. All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | 1. Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | 2. Redistributions in binary form must reproduce the above copyright 19 | notice, this list of conditions and the following disclaimer in the 20 | documentation and/or other materials provided with the distribution. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 25 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 26 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 27 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 28 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 29 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 30 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /internal/xdp/headers/bpf_endian.h: -------------------------------------------------------------------------------- 1 | /* SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) */ 2 | #ifndef __BPF_ENDIAN__ 3 | #define __BPF_ENDIAN__ 4 | 5 | /* 6 | * Isolate byte #n and put it into byte #m, for __u##b type. 7 | * E.g., moving byte #6 (nnnnnnnn) into byte #1 (mmmmmmmm) for __u64: 8 | * 1) xxxxxxxx nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 9 | * 2) nnnnnnnn xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx mmmmmmmm xxxxxxxx 00000000 10 | * 3) 00000000 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 11 | * 4) 00000000 00000000 00000000 00000000 00000000 00000000 nnnnnnnn 00000000 12 | */ 13 | #define ___bpf_mvb(x, b, n, m) ((__u##b)(x) << (b-(n+1)*8) >> (b-8) << (m*8)) 14 | 15 | #define ___bpf_swab16(x) ((__u16)( \ 16 | ___bpf_mvb(x, 16, 0, 1) | \ 17 | ___bpf_mvb(x, 16, 1, 0))) 18 | 19 | #define ___bpf_swab32(x) ((__u32)( \ 20 | ___bpf_mvb(x, 32, 0, 3) | \ 21 | ___bpf_mvb(x, 32, 1, 2) | \ 22 | ___bpf_mvb(x, 32, 2, 1) | \ 23 | ___bpf_mvb(x, 32, 3, 0))) 24 | 25 | #define ___bpf_swab64(x) ((__u64)( \ 26 | ___bpf_mvb(x, 64, 0, 7) | \ 27 | ___bpf_mvb(x, 64, 1, 6) | \ 28 | ___bpf_mvb(x, 64, 2, 5) | \ 29 | ___bpf_mvb(x, 64, 3, 4) | \ 30 | ___bpf_mvb(x, 64, 4, 3) | \ 31 | ___bpf_mvb(x, 64, 5, 2) | \ 32 | ___bpf_mvb(x, 64, 6, 1) | \ 33 | ___bpf_mvb(x, 64, 7, 0))) 34 | 35 | /* LLVM's BPF target selects the endianness of the CPU 36 | * it compiles on, or the user specifies (bpfel/bpfeb), 37 | * respectively. The used __BYTE_ORDER__ is defined by 38 | * the compiler, we cannot rely on __BYTE_ORDER from 39 | * libc headers, since it doesn't reflect the actual 40 | * requested byte order. 41 | * 42 | * Note, LLVM's BPF target has different __builtin_bswapX() 43 | * semantics. It does map to BPF_ALU | BPF_END | BPF_TO_BE 44 | * in bpfel and bpfeb case, which means below, that we map 45 | * to cpu_to_be16(). We could use it unconditionally in BPF 46 | * case, but better not rely on it, so that this header here 47 | * can be used from application and BPF program side, which 48 | * use different targets. 49 | */ 50 | #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ 51 | # define __bpf_ntohs(x) __builtin_bswap16(x) 52 | # define __bpf_htons(x) __builtin_bswap16(x) 53 | # define __bpf_constant_ntohs(x) ___bpf_swab16(x) 54 | # define __bpf_constant_htons(x) ___bpf_swab16(x) 55 | # define __bpf_ntohl(x) __builtin_bswap32(x) 56 | # define __bpf_htonl(x) __builtin_bswap32(x) 57 | # define __bpf_constant_ntohl(x) ___bpf_swab32(x) 58 | # define __bpf_constant_htonl(x) ___bpf_swab32(x) 59 | # define __bpf_be64_to_cpu(x) __builtin_bswap64(x) 60 | # define __bpf_cpu_to_be64(x) __builtin_bswap64(x) 61 | # define __bpf_constant_be64_to_cpu(x) ___bpf_swab64(x) 62 | # define __bpf_constant_cpu_to_be64(x) ___bpf_swab64(x) 63 | #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__ 64 | # define __bpf_ntohs(x) (x) 65 | # define __bpf_htons(x) (x) 66 | # define __bpf_constant_ntohs(x) (x) 67 | # define __bpf_constant_htons(x) (x) 68 | # define __bpf_ntohl(x) (x) 69 | # define __bpf_htonl(x) (x) 70 | # define __bpf_constant_ntohl(x) (x) 71 | # define __bpf_constant_htonl(x) (x) 72 | # define __bpf_be64_to_cpu(x) (x) 73 | # define __bpf_cpu_to_be64(x) (x) 74 | # define __bpf_constant_be64_to_cpu(x) (x) 75 | # define __bpf_constant_cpu_to_be64(x) (x) 76 | #else 77 | # error "Fix your compiler's __BYTE_ORDER__?!" 78 | #endif 79 | 80 | #define bpf_htons(x) \ 81 | (__builtin_constant_p(x) ? \ 82 | __bpf_constant_htons(x) : __bpf_htons(x)) 83 | #define bpf_ntohs(x) \ 84 | (__builtin_constant_p(x) ? \ 85 | __bpf_constant_ntohs(x) : __bpf_ntohs(x)) 86 | #define bpf_htonl(x) \ 87 | (__builtin_constant_p(x) ? \ 88 | __bpf_constant_htonl(x) : __bpf_htonl(x)) 89 | #define bpf_ntohl(x) \ 90 | (__builtin_constant_p(x) ? \ 91 | __bpf_constant_ntohl(x) : __bpf_ntohl(x)) 92 | #define bpf_cpu_to_be64(x) \ 93 | (__builtin_constant_p(x) ? \ 94 | __bpf_constant_cpu_to_be64(x) : __bpf_cpu_to_be64(x)) 95 | #define bpf_be64_to_cpu(x) \ 96 | (__builtin_constant_p(x) ? \ 97 | __bpf_constant_be64_to_cpu(x) : __bpf_be64_to_cpu(x)) 98 | 99 | #endif /* __BPF_ENDIAN__ */ 100 | -------------------------------------------------------------------------------- /internal/xdp/headers/update.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Version of libbpf to fetch headers from 4 | LIBBPF_VERSION=1.0.0 5 | 6 | # The headers we want 7 | prefix=libbpf-"$LIBBPF_VERSION" 8 | headers=( 9 | "$prefix"/LICENSE.BSD-2-Clause 10 | "$prefix"/src/bpf_endian.h 11 | "$prefix"/src/bpf_helper_defs.h 12 | "$prefix"/src/bpf_helpers.h 13 | ) 14 | 15 | # Fetch libbpf release and extract the desired headers 16 | curl -sL "https://github.com/libbpf/libbpf/archive/refs/tags/v${LIBBPF_VERSION}.tar.gz" | \ 17 | tar -xz --xform='s#.*/##' "${headers[@]}" 18 | -------------------------------------------------------------------------------- /internal/xdp/xdp.go: -------------------------------------------------------------------------------- 1 | package xdp 2 | 3 | type Mode uint8 4 | 5 | const ( 6 | ModeUndefined Mode = iota 7 | ModeSKB 8 | ModeDriver 9 | // ModeHW 10 | ) 11 | 12 | func (m Mode) Valid() bool { 13 | //nolint:exhaustive 14 | switch m { 15 | // case ModeSKB, ModeDriver, ModeHW: 16 | case ModeSKB, ModeDriver: 17 | return true 18 | default: 19 | return false 20 | } 21 | } 22 | 23 | func ModeFromString(s string) (m Mode, ok bool) { 24 | switch s { 25 | case "skb": 26 | return ModeSKB, true 27 | case "driver": 28 | return ModeDriver, true 29 | default: 30 | return ModeUndefined, false 31 | } 32 | } 33 | 34 | type Action uint8 35 | 36 | const ( 37 | ActionAborted Action = iota 38 | ActionDrop 39 | ActionPass 40 | ActionTX 41 | ActionRedirect 42 | ) 43 | 44 | func (x Action) String() string { 45 | switch x { 46 | case ActionAborted: 47 | return "XDP_ABORTED" 48 | case ActionDrop: 49 | return "XDP_DROP" 50 | case ActionPass: 51 | return "XDP_PASS" 52 | case ActionTX: 53 | return "XDP_TX" 54 | case ActionRedirect: 55 | return "XDP_REDIRECT" 56 | default: 57 | return "" 58 | } 59 | } 60 | 61 | func (x Action) Uint32() uint32 { 62 | return uint32(x) 63 | } 64 | 65 | type OSPAStatID uint8 66 | 67 | const ( 68 | OSPAStatIDNotOpenSPAPacket OSPAStatID = iota 69 | OSPAStatIDADKProofInvalid 70 | OSPAStatIDADKProofValid 71 | ) 72 | 73 | func (o OSPAStatID) Uint32() uint32 { 74 | return uint32(o) 75 | } 76 | -------------------------------------------------------------------------------- /pkg/openspalib/adk.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/pquerna/otp/totp" 11 | ) 12 | 13 | const ( 14 | ADKSecretLen = ADKLength // in bytes 15 | ADKSecretEncodedLen = 7 16 | totpPeriod = 60 // in seconds 17 | ) 18 | 19 | var b32NoPadding = base32.StdEncoding.WithPadding(base32.NoPadding) 20 | 21 | func ADKGenerateSecret() (string, error) { 22 | secret := make([]byte, ADKSecretLen) 23 | n, err := rand.Read(secret) 24 | if err != nil { 25 | return "", errors.Wrap(err, "random number generation") 26 | } 27 | 28 | if n != ADKSecretLen { 29 | return "", errors.New("invalid random read length") 30 | } 31 | 32 | s := b32NoPadding.EncodeToString(secret) 33 | 34 | return s, nil 35 | } 36 | 37 | func ADKGenerateProof(secret string) (uint32, error) { 38 | return ADKGenerateProofCustom(secret, time.Now()) 39 | } 40 | 41 | func ADKGenerateNextProof(secret string) (uint32, error) { 42 | return ADKGenerateProofCustom(secret, time.Now().Add(time.Second*totpPeriod)) 43 | } 44 | 45 | func ADKGenerateProofCustom(secret string, t time.Time) (uint32, error) { 46 | passcode, err := totp.GenerateCodeCustom(secret, t, totp.ValidateOpts{ 47 | Period: totpPeriod, 48 | Skew: 1, 49 | Digits: 9, 50 | }) 51 | 52 | if err != nil { 53 | return 0, errors.Wrap(err, "totp generate") 54 | } 55 | 56 | p, err := strconv.Atoi(passcode) 57 | if err != nil { 58 | return 0, errors.Wrap(err, "strconv") 59 | } 60 | 61 | return uint32(p), nil 62 | } 63 | 64 | // ADKProver is a cached version of the ADKGenerateProof function, which recalculates the proof when the cached 65 | // version is older than a second. This avoids calculating the same proof for every single packet and instead 66 | // calculating the proof at least every second opposed to multiple times per second (when receiving multiple packets 67 | // with a second). Run the benchmarks to see the speedup numbers for your setup. 68 | type ADKProver struct { 69 | secret string 70 | 71 | // last time the proof was calculated 72 | last time.Time 73 | proof uint32 74 | } 75 | 76 | func NewADKProver(secret string) (ADKProver, error) { 77 | proof, err := ADKGenerateProof(secret) 78 | if err != nil { 79 | return ADKProver{}, errors.Wrap(err, "adk generate proof") 80 | } 81 | 82 | return ADKProver{ 83 | last: time.Now(), 84 | proof: proof, 85 | secret: secret, 86 | }, nil 87 | } 88 | 89 | func (a *ADKProver) Proof() (uint32, error) { 90 | n := time.Now() 91 | if n.Sub(a.last).Seconds() > 1 { 92 | p, err := ADKGenerateProof(a.secret) 93 | if err != nil { 94 | return 0, err 95 | } 96 | a.proof = p 97 | } 98 | 99 | return a.proof, nil 100 | } 101 | 102 | var ErrADKProofMismatch = errors.New("adk proof mismatch") 103 | 104 | // Valid compares the inputted proof with the actual proof, verifying that the inputted ADK proof is valid. 105 | func (a *ADKProver) Valid(proof uint32) error { 106 | p, err := a.Proof() 107 | if err != nil { 108 | return errors.Wrap(err, "proof generation") 109 | } 110 | 111 | if p != proof { 112 | return ErrADKProofMismatch 113 | } 114 | 115 | return nil 116 | } 117 | -------------------------------------------------------------------------------- /pkg/openspalib/adk_test.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestADKGenerateSecret(t *testing.T) { 12 | assert.Equal(t, 4, ADKLength) 13 | 14 | for i := 0; i < 100; i++ { 15 | k, err := ADKGenerateSecret() 16 | require.NoErrorf(t, err, "Iteration %d", i) 17 | require.Lenf(t, k, ADKSecretEncodedLen, "Iteration %d", i) 18 | } 19 | } 20 | 21 | func TestADKGenerateProof(t *testing.T) { 22 | s, err := ADKGenerateSecret() 23 | assert.NoError(t, err) 24 | 25 | proof0, err := ADKGenerateProof(s) 26 | assert.NoError(t, err) 27 | assert.NotEqual(t, uint32(0), proof0) 28 | 29 | proof1, err := ADKGenerateNextProof(s) 30 | assert.NoError(t, err) 31 | assert.NotEqual(t, uint32(0), proof1) 32 | 33 | for i := 0; i < 10; i++ { 34 | proof, err := ADKGenerateProof(s) 35 | assert.NoError(t, err) 36 | assert.Equal(t, proof0, proof) 37 | assert.NotEqual(t, proof0, proof1) 38 | } 39 | } 40 | 41 | func TestADKProver(t *testing.T) { 42 | s, err := ADKGenerateSecret() 43 | assert.NoError(t, err) 44 | 45 | pc, err := NewADKProver(s) 46 | assert.NoError(t, err) 47 | 48 | proof0, err := ADKGenerateProof(s) 49 | assert.NoError(t, err) 50 | 51 | pc0, err := pc.Proof() 52 | assert.NoError(t, err) 53 | assert.NotEqual(t, uint32(0), pc0) 54 | assert.Equal(t, proof0, pc0) 55 | assert.NotEqualf(t, time.Time{}, pc.last, "time field last was not updated") 56 | 57 | for i := 0; i < 10; i++ { 58 | proof, err := pc.Proof() 59 | assert.NoError(t, err) 60 | assert.Equal(t, proof0, proof) 61 | } 62 | } 63 | 64 | func TestADKProver_Valid(t *testing.T) { 65 | s, err := ADKGenerateSecret() 66 | assert.NoError(t, err) 67 | 68 | pc, err := NewADKProver(s) 69 | assert.NoError(t, err) 70 | 71 | proof, err := ADKGenerateProof(s) 72 | assert.NoError(t, err) 73 | assert.NoError(t, pc.Valid(proof)) 74 | } 75 | 76 | func BenchmarkADKGenerateProof(b *testing.B) { 77 | s, err := ADKGenerateSecret() 78 | assert.NoError(b, err) 79 | 80 | b.ResetTimer() 81 | 82 | for i := 0; i < b.N; i++ { 83 | _, _ = ADKGenerateProof(s) 84 | } 85 | } 86 | 87 | func BenchmarkADKProver(b *testing.B) { 88 | s, err := ADKGenerateSecret() 89 | assert.NoError(b, err) 90 | 91 | pc, err := NewADKProver(s) 92 | assert.NoError(b, err) 93 | 94 | b.ResetTimer() 95 | 96 | for i := 0; i < b.N; i++ { 97 | _, _ = pc.Proof() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /pkg/openspalib/common.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | uuid "github.com/satori/go.uuid" 8 | ) 9 | 10 | var ( 11 | ErrInvalidBytes = errors.New("invalid bytes") 12 | ErrMissingEntry = errors.New("missing entry") 13 | ErrBadInput = errors.New("bad input") 14 | ErrViolationOfProtocolSpec = errors.New("violation of protocol spec") 15 | ErrCipherSuiteRequired = errors.New("cipher suite required") 16 | ErrPDUTooLarge = errors.New("pdu too large") 17 | ) 18 | 19 | const ( 20 | DefaultServerPort = 22211 21 | MaxPDUSize = 1444 22 | ) 23 | 24 | var ( 25 | ProtocolUndefined = InternetProtocolNumber{ 26 | Number: 0, 27 | Protocol: "", 28 | } 29 | ProtocolICMP = InternetProtocolNumber{ 30 | Number: 1, 31 | Protocol: "ICMP", 32 | } 33 | ProtocolIPV4 = InternetProtocolNumber{ 34 | Number: 4, 35 | Protocol: "IPv4", 36 | } 37 | ProtocolTCP = InternetProtocolNumber{ 38 | Number: 6, 39 | Protocol: "TCP", 40 | } 41 | ProtocolUDP = InternetProtocolNumber{ 42 | Number: 17, 43 | Protocol: "UDP", 44 | } 45 | ProtocolICMPv6 = InternetProtocolNumber{ 46 | Number: 58, 47 | Protocol: "ICMPv6", 48 | } 49 | ) 50 | 51 | // InternetProtocolNumber is the protocol found in the IPv4 header field Protocol. 52 | // See: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml 53 | type InternetProtocolNumber struct { 54 | Number uint8 55 | Protocol string 56 | } 57 | 58 | func (i InternetProtocolNumber) ToBin() byte { 59 | return i.Number 60 | } 61 | 62 | func (i InternetProtocolNumber) String() string { 63 | return i.Protocol 64 | } 65 | 66 | // InternetProtocolNumberSupported returns a slice of InternetProtocolNumber that are supported. 67 | func InternetProtocolNumberSupported() []InternetProtocolNumber { 68 | return []InternetProtocolNumber{ 69 | ProtocolICMP, 70 | ProtocolIPV4, 71 | ProtocolTCP, 72 | ProtocolUDP, 73 | ProtocolICMPv6, 74 | } 75 | } 76 | 77 | func InternetProtocolFromString(s string) (InternetProtocolNumber, error) { 78 | for _, p := range InternetProtocolNumberSupported() { 79 | if strings.EqualFold(p.Protocol, s) { 80 | proto := p 81 | return proto, nil 82 | } 83 | } 84 | 85 | return ProtocolUndefined, errors.New("non supported protocol") 86 | } 87 | 88 | func InternetProtocolFromNumber(i uint8) (InternetProtocolNumber, error) { 89 | for _, p := range InternetProtocolNumberSupported() { 90 | if p.Number == i { 91 | proto := p 92 | return proto, nil 93 | } 94 | } 95 | 96 | return ProtocolUndefined, errors.New("non supported protocol") 97 | } 98 | 99 | // portCanBeZero returns if the port can be equal to zero for the specified protocol. 100 | func portCanBeZero(protocol InternetProtocolNumber) bool { 101 | switch protocol { 102 | case ProtocolTCP, ProtocolUDP: 103 | return false 104 | default: 105 | return true 106 | } 107 | } 108 | 109 | func RandomUUID() string { 110 | return uuid.NewV4().String() 111 | } 112 | -------------------------------------------------------------------------------- /pkg/openspalib/common_test.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestPortCanBeZero(t *testing.T) { 10 | assert.False(t, portCanBeZero(ProtocolTCP)) 11 | assert.False(t, portCanBeZero(ProtocolUDP)) 12 | assert.True(t, portCanBeZero(ProtocolICMP)) 13 | assert.True(t, portCanBeZero(ProtocolIPV4)) 14 | assert.True(t, portCanBeZero(ProtocolICMPv6)) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/aes_256_cbc.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "crypto/rand" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func AES256CBCEncrypt(plaintext []byte) (ciphertext, iv, key []byte, err error) { 12 | iv = make([]byte, 16) 13 | if _, err2 := rand.Read(iv); err2 != nil { 14 | err = errors.Wrap(err2, "random iv generation") 15 | return 16 | } 17 | 18 | key = make([]byte, 32) 19 | if _, err2 := rand.Read(key); err2 != nil { 20 | err = errors.Wrap(err2, "random key generation") 21 | return 22 | } 23 | 24 | e := NewAES256CBCEncrypter(iv, key) 25 | ciphertext, err = e.Encrypt(plaintext) 26 | return 27 | } 28 | 29 | var _ Encrypter = AES256CBCEncrypter{} 30 | 31 | type AES256CBCEncrypter struct { 32 | iv []byte 33 | key []byte 34 | } 35 | 36 | func NewAES256CBCEncrypter(iv, key []byte) *AES256CBCEncrypter { 37 | a := &AES256CBCEncrypter{ 38 | iv: iv, 39 | key: key, 40 | } 41 | return a 42 | } 43 | 44 | func (a AES256CBCEncrypter) Encrypt(plaintext []byte) (ciphertext []byte, err error) { 45 | return aes256CBCEncrypt(plaintext, a.key, a.iv) 46 | } 47 | 48 | func aes256CBCEncrypt(plaintext, key, iv []byte) (ciphertext []byte, err error) { 49 | // Pad the data 50 | dataPadded, err := PaddingPKCS7(plaintext, aes.BlockSize) 51 | if err != nil { 52 | return nil, errors.Wrap(err, "padding pkcs7") 53 | } 54 | 55 | return _aes256CBCEncrypt(dataPadded, key, iv) 56 | } 57 | 58 | func _aes256CBCEncrypt(plaintext, key, iv []byte) (ciphertext []byte, err error) { 59 | const ivSize = aes.BlockSize 60 | 61 | // check just to be sure that the plaintext body is a multiple of the AES block size 62 | if len(plaintext)%aes.BlockSize != 0 { 63 | return nil, errors.New("plaintext not a multiple of the block size") 64 | } 65 | 66 | if len(iv) != ivSize { 67 | return nil, errors.New("invalid IV size") 68 | } 69 | 70 | block, err := aes.NewCipher(key) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | c := make([]byte, len(plaintext)) 76 | 77 | mode := cipher.NewCBCEncrypter(block, iv) 78 | mode.CryptBlocks(c, plaintext) 79 | 80 | return c, nil 81 | } 82 | 83 | var _ Decrypter = AES256CBCDecrypter{} 84 | 85 | type AES256CBCDecrypter struct { 86 | iv []byte 87 | key []byte 88 | } 89 | 90 | func NewAES256CBCDecrypter(iv, key []byte) *AES256CBCDecrypter { 91 | a := &AES256CBCDecrypter{ 92 | iv: iv, 93 | key: key, 94 | } 95 | return a 96 | } 97 | 98 | func (a AES256CBCDecrypter) Decrypt(ciphertext []byte) (plaintext []byte, err error) { 99 | return aes256CBCDecrypt(ciphertext, a.iv, a.key) 100 | } 101 | 102 | func aes256CBCDecrypt(ciphertext, iv, key []byte) (plaintext []byte, err error) { 103 | padded, err := _aes256CBCDecrypt(ciphertext, iv, key) 104 | if err != nil { 105 | return nil, errors.Wrap(err, "aes256cbc decrypt") 106 | } 107 | 108 | plaintext, err = PaddingPKCS7Remove(padded, aes.BlockSize) 109 | if err != nil { 110 | return nil, errors.Wrap(err, "remove pkcs7 padding") 111 | } 112 | 113 | return plaintext, nil 114 | } 115 | 116 | func _aes256CBCDecrypt(ciphertext, iv, key []byte) ([]byte, error) { 117 | if len(ciphertext)%aes.BlockSize != 0 { 118 | return nil, errors.New("tried to decrypt using AES-256-CBC ciphertext that is not a multiple of the block size") 119 | } 120 | 121 | block, err := aes.NewCipher(key) 122 | 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | plaintext := make([]byte, len(ciphertext)) 128 | mode := cipher.NewCBCDecrypter(block, iv) 129 | mode.CryptBlocks(plaintext, ciphertext) // will decrypt in-place 130 | 131 | return plaintext, nil 132 | } 133 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/aes_256_cbc_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAES256CBCEncrypterDecrypter(t *testing.T) { 11 | iv := []byte{0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90} 12 | key := []byte{0x6b, 0x4b, 0x0d, 0x32, 0x55, 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xda, 0x39, 0xa3, 0xee, 0x5e, 13 | 0xbf, 0xef, 0x95, 0x60, 0x18, 0x90, 0xda, 0x39, 0xa3, 0xee, 0x5e, 0x6b, 0x4b, 0x0d, 0x32, 0x55} 14 | content := []byte("Hello SPA World!") 15 | 16 | assert.Len(t, iv, 16) 17 | assert.Len(t, key, 32) 18 | 19 | // Encrypt 20 | e := NewAES256CBCEncrypter(iv, key) 21 | cipher, err := e.Encrypt(content) 22 | assert.NoError(t, err) 23 | 24 | assert.Len(t, cipher, 32) 25 | cipherExpect := []byte{0x9B, 0x60, 0x3B, 0xFD, 0xD7, 0x5D, 0xC1, 0x14, 0x6D, 0x22, 0xB0, 0x44, 0xDA, 0x66, 0x97, 0x18, 26 | 0xFD, 0xAD, 0xE8, 0x28, 0x1D, 0x61, 0xEF, 0xBD, 0xE0, 0x3E, 0x30, 0x5D, 0xE9, 0xA3, 0x9F, 0xEA} 27 | assert.Equal(t, cipherExpect, cipher) 28 | assert.False(t, strings.Contains(string(cipher), string(content))) 29 | 30 | // Decrypt 31 | d := NewAES256CBCDecrypter(iv, key) 32 | plain, err := d.Decrypt(cipher) 33 | assert.NoError(t, err) 34 | assert.Equal(t, content, plain) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/cipher_suite.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 7 | ) 8 | 9 | type CipherSuiteID uint8 10 | 11 | //nolint:revive,stylecheck 12 | const ( 13 | CipherUnknown CipherSuiteID = 0 // only to be used for development 14 | CipherNoSecurity CipherSuiteID = 255 // only to be used for development 15 | CipherRSA_SHA256_AES256CBC_ID CipherSuiteID = 1 16 | ) 17 | 18 | type CipherSuite interface { 19 | CipherSuiteID() CipherSuiteID 20 | 21 | // Secure performs Encryption (body) and SignatureSignor (header+body) and returns an Encrypted TLV container. 22 | // The meta parameter is additional information that is available for security, it is not actually sent to the 23 | // recipient. 24 | Secure(header []byte, packet, meta tlv.Container) (tlv.Container, error) 25 | 26 | // Unlock performs Decryption and SignatureSignor verification and returns the Packet TLV container that was secured 27 | Unlock(header []byte, ec tlv.Container) (tlv.Container, error) 28 | } 29 | 30 | type Encryption interface { 31 | Encrypter 32 | Decrypter 33 | } 34 | 35 | type Signature interface { 36 | SignatureSignor 37 | SignatureVerifier 38 | } 39 | 40 | type Encrypter interface { 41 | Encrypt(plaintext []byte) (ciphertext []byte, err error) 42 | } 43 | 44 | type Decrypter interface { 45 | Decrypt(ciphertext []byte) (plaintext []byte, err error) 46 | } 47 | 48 | type SignatureSignor interface { 49 | Sign(data []byte) (signature []byte, err error) 50 | } 51 | 52 | type SignatureVerifier interface { 53 | Verify(text, signature []byte) (valid bool, err error) 54 | } 55 | 56 | func CipherSuiteStringToID(s string) CipherSuiteID { 57 | switch s { 58 | case "CipherSuite_NoSecurity": 59 | return CipherNoSecurity 60 | case "CipherSuite_RSA_SHA256_AES256CBC": 61 | return CipherRSA_SHA256_AES256CBC_ID 62 | default: 63 | return CipherUnknown 64 | } 65 | } 66 | 67 | func CipherSuiteIDToString(c CipherSuiteID) (string, error) { 68 | switch c { 69 | case CipherNoSecurity: 70 | return "CipherSuite_NoSecurity", nil 71 | case CipherRSA_SHA256_AES256CBC_ID: 72 | return "CipherSuite_RSA_SHA256_AES256CBC", nil 73 | case CipherUnknown: 74 | return "", errors.New("unknown cipher suite id") 75 | default: 76 | return "", errors.New("unsupported cipher suite id") 77 | } 78 | } 79 | 80 | func MustCipherSuiteIDToString(c CipherSuiteID) string { 81 | s, err := CipherSuiteIDToString(c) 82 | if err != nil { 83 | panic(err) 84 | } 85 | return s 86 | } 87 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/common.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "errors" 7 | 8 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 9 | ) 10 | 11 | // PublicKeyLookuper is used when we need to get the client's public key based on their clientUUID. The client's public 12 | // key will be used to encrypt OpenSPA responses and verify signatures from OpenSPA requests. If the client is not 13 | // authorized, this function should still return their key, since the authentication step is performed separately. 14 | type PublicKeyLookuper interface { 15 | LookupPublicKey(clientUUID string) (crypto.PublicKey, error) 16 | } 17 | 18 | type PublicKeyResolver interface { 19 | PublicKey(packet, meta tlv.Container) (crypto.PublicKey, error) 20 | } 21 | 22 | func PaddingPKCS7(data []byte, blockSize int) ([]byte, error) { 23 | if blockSize >= 256 || blockSize < 0 { 24 | return nil, errors.New("invalid block size") 25 | } 26 | 27 | size := blockSize - (len(data) % blockSize) 28 | if size == 0 { 29 | size = blockSize 30 | } 31 | 32 | padding := make([]byte, size) 33 | 34 | for i := 0; i < size; i++ { 35 | padding[i] = byte(size) 36 | } 37 | 38 | dataPadded := make([]byte, len(data), len(data)+size) 39 | copy(dataPadded, data) 40 | dataPadded = append(dataPadded, padding...) 41 | 42 | return dataPadded, nil 43 | } 44 | 45 | func PaddingPKCS7Remove(data []byte, blockSize int) ([]byte, error) { 46 | if blockSize >= 256 || blockSize < 0 { 47 | return nil, errors.New("invalid block size") 48 | } 49 | 50 | if len(data) == 0 { 51 | return nil, errors.New("empty data") 52 | } 53 | 54 | s := int(data[len(data)-1]) 55 | if s > len(data) { 56 | return nil, errors.New("padding length is larger than input data slice") 57 | } 58 | 59 | pIdx := len(data) - s 60 | 61 | for i := len(data) - 1; i >= pIdx; i-- { 62 | if int(data[i]) != s { 63 | return nil, errors.New("padding value is not consistent") 64 | } 65 | } 66 | 67 | return data[:pIdx], nil 68 | } 69 | 70 | const nonceMinSize = 3 // Minimum number of bytes for the nocne 71 | 72 | func randomNonce(size int) ([]byte, error) { 73 | b := make([]byte, size) 74 | _, err := rand.Read(b) 75 | if err != nil { 76 | return nil, err 77 | } 78 | return b, nil 79 | } 80 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/common_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto/rand" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestPaddingPKCS7(t *testing.T) { 12 | blockSize := 16 // bytes 13 | rnd := make([]byte, blockSize) 14 | n, err := rand.Read(rnd) 15 | assert.NoError(t, err) 16 | assert.Equal(t, blockSize, n) 17 | 18 | for dLen := 1; dLen < 15; dLen++ { 19 | // Pad 20 | test := rnd[:dLen] 21 | testOut, err := PaddingPKCS7(test, blockSize) 22 | 23 | assert.NoError(t, err) 24 | 25 | for j := 0; j < dLen; j++ { 26 | assert.Equal(t, rnd[j], testOut[j]) 27 | } 28 | 29 | assert.Len(t, testOut, blockSize) 30 | 31 | for i := dLen; i < len(testOut); i++ { 32 | assert.Equal(t, byte(blockSize-dLen), testOut[i]) 33 | } 34 | 35 | // Remove Padding 36 | removedPadding, err := PaddingPKCS7Remove(testOut, blockSize) 37 | assert.NoError(t, err) 38 | assert.Equal(t, test, removedPadding) 39 | } 40 | 41 | // Test if no padding is required, the padded output is 2 block sizes. 42 | testOut, err := PaddingPKCS7(rnd, blockSize) 43 | assert.NoError(t, err) 44 | require.Len(t, testOut, blockSize*2) 45 | 46 | for i := 0; i < blockSize; i++ { 47 | assert.Equal(t, rnd[i], testOut[i]) 48 | } 49 | 50 | for i := 0; i < blockSize; i++ { 51 | assert.Equal(t, byte(blockSize), testOut[blockSize+i]) 52 | } 53 | 54 | // Remove Padding 55 | removedPadding, err := PaddingPKCS7Remove(testOut, blockSize) 56 | assert.NoError(t, err) 57 | assert.Equal(t, rnd, removedPadding) 58 | } 59 | 60 | func TestPaddingPKCS7_EmptySlice(t *testing.T) { 61 | blockSize := 16 // bytes 62 | expect := []byte{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16} 63 | 64 | res, err := PaddingPKCS7([]byte{}, blockSize) 65 | assert.NoError(t, err) 66 | assert.Equal(t, expect, res) 67 | } 68 | 69 | func TestPaddingPKCS7Remove_IncorrectPaddingValue(t *testing.T) { 70 | blockSize := 16 // bytes 71 | d := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 2} 72 | assert.Len(t, d, blockSize) 73 | 74 | res, err := PaddingPKCS7Remove(d, blockSize) 75 | assert.Nil(t, res) 76 | assert.Error(t, err) 77 | } 78 | 79 | func TestPaddingPKCS7Remove_PaddingValueOutOfRange(t *testing.T) { 80 | blockSize := 16 // bytes 81 | d := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17} 82 | assert.Len(t, d, blockSize) 83 | 84 | res, err := PaddingPKCS7Remove(d, blockSize) 85 | assert.Nil(t, res) 86 | assert.Error(t, err) 87 | } 88 | 89 | func TestPaddingPKCS7Remove_PaddingEmptySlice(t *testing.T) { 90 | blockSize := 16 // bytes 91 | d := []byte{16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16} 92 | assert.Len(t, d, blockSize) 93 | 94 | res, err := PaddingPKCS7Remove(d, blockSize) 95 | assert.Len(t, res, 0) 96 | assert.NoError(t, err) 97 | } 98 | 99 | func TestRandomNonce(t *testing.T) { 100 | bx := make([][]byte, 0) 101 | for i := 0; i < 100; i++ { 102 | b, err := randomNonce(3) 103 | assert.NoError(t, err) 104 | 105 | for j := 0; j < len(bx); j++ { 106 | assert.NotEqual(t, bx[j], b) 107 | } 108 | 109 | bx = append(bx, b) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/mocks_and_stubs.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "crypto" 5 | 6 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 7 | "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | var _ CipherSuite = &CipherSuiteMock{} 11 | 12 | type CipherSuiteMock struct { 13 | mock.Mock 14 | } 15 | 16 | func NewCipherSuiteMock() *CipherSuiteMock { 17 | c := &CipherSuiteMock{} 18 | return c 19 | } 20 | 21 | func (c *CipherSuiteMock) CipherSuiteID() CipherSuiteID { 22 | args := c.Called() 23 | return CipherSuiteID(uint8(args.Int(0))) 24 | } 25 | 26 | func (c *CipherSuiteMock) Secure(header []byte, body, meta tlv.Container) (tlv.Container, error) { 27 | args := c.Called(header, body, meta) 28 | return args.Get(0).(tlv.Container), args.Error(1) 29 | } 30 | 31 | func (c *CipherSuiteMock) Unlock(header []byte, ec tlv.Container) (tlv.Container, error) { 32 | args := c.Called(header, ec) 33 | return args.Get(0).(tlv.Container), args.Error(1) 34 | } 35 | 36 | func (c *CipherSuiteMock) Encrypt(plaintext []byte) (ciphertext []byte, err error) { 37 | args := c.Called(plaintext) 38 | return args.Get(0).([]byte), args.Error(1) 39 | } 40 | 41 | func (c *CipherSuiteMock) Decrypt(ciphertext []byte) (plaintext []byte, err error) { 42 | args := c.Called(ciphertext) 43 | return args.Get(0).([]byte), args.Error(1) 44 | } 45 | 46 | func (c *CipherSuiteMock) Sign(data []byte) (signature []byte, err error) { 47 | args := c.Called(data) 48 | return args.Get(0).([]byte), args.Error(1) 49 | } 50 | 51 | func (c *CipherSuiteMock) Verify(text, signature []byte) (valid bool, err error) { 52 | args := c.Called(text, signature) 53 | return args.Bool(0), args.Error(1) 54 | } 55 | 56 | var _ CipherSuite = &CipherSuiteStub{} 57 | 58 | type CipherSuiteStub struct{} 59 | 60 | func NewCipherSuiteStub() *CipherSuiteStub { 61 | c := &CipherSuiteStub{} 62 | return c 63 | } 64 | 65 | func (c *CipherSuiteStub) CipherSuiteID() CipherSuiteID { 66 | return CipherNoSecurity 67 | } 68 | 69 | func (c *CipherSuiteStub) Secure(header []byte, body, meta tlv.Container) (tlv.Container, error) { 70 | return body, nil 71 | } 72 | 73 | func (c *CipherSuiteStub) Unlock(header []byte, ec tlv.Container) (tlv.Container, error) { 74 | return ec, nil 75 | } 76 | 77 | var _ PublicKeyLookuper = &PublicKeyLookupMock{} 78 | 79 | type PublicKeyLookupMock struct { 80 | mock.Mock 81 | } 82 | 83 | func NewPublicKeyLookupMock() *PublicKeyLookupMock { 84 | p := &PublicKeyLookupMock{} 85 | return p 86 | } 87 | 88 | func (p *PublicKeyLookupMock) LookupPublicKey(clientUUID string) (crypto.PublicKey, error) { 89 | args := p.Called(clientUUID) 90 | return args.Get(0).(crypto.PublicKey), args.Error(1) 91 | } 92 | 93 | var _ PublicKeyResolver = &PublicKeyResolverMock{} 94 | 95 | type PublicKeyResolverMock struct { 96 | mock.Mock 97 | } 98 | 99 | func NewPublicKeyResolverMock() *PublicKeyResolverMock { 100 | p := &PublicKeyResolverMock{} 101 | return p 102 | } 103 | 104 | func (p *PublicKeyResolverMock) PublicKey(packet, meta tlv.Container) (crypto.PublicKey, error) { 105 | args := p.Called(packet, meta) 106 | return args.Get(0).(crypto.PublicKey), args.Error(1) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/rsa.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/ecdsa" 7 | "crypto/rand" 8 | "crypto/rsa" 9 | "crypto/sha256" 10 | "crypto/x509" 11 | "encoding/pem" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | type RSAEncrypter struct { 17 | pubkey *rsa.PublicKey 18 | } 19 | 20 | func NewRSAEncrypter(pubkey *rsa.PublicKey) *RSAEncrypter { 21 | r := &RSAEncrypter{ 22 | pubkey: pubkey, 23 | } 24 | return r 25 | } 26 | 27 | func (r *RSAEncrypter) Encrypt(plaintext []byte) (ciphertext []byte, err error) { 28 | if len(plaintext) == 0 { 29 | return nil, errors.New("cannot encrypt empty byte slice") 30 | } 31 | 32 | ciphertext, err = rsa.EncryptPKCS1v15(rand.Reader, r.pubkey, plaintext) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | return ciphertext, nil 38 | } 39 | 40 | type RSADecrypter struct { 41 | privkey *rsa.PrivateKey 42 | } 43 | 44 | func NewRSADecrypter(privkey *rsa.PrivateKey) *RSADecrypter { 45 | r := &RSADecrypter{ 46 | privkey: privkey, 47 | } 48 | return r 49 | } 50 | 51 | func (r *RSADecrypter) Decrypt(ciphertext []byte) (plaintext []byte, err error) { 52 | if len(ciphertext) == 0 { 53 | return nil, errors.New("cannot decrypt empty byte slice") 54 | } 55 | 56 | plaintext, err = rsa.DecryptPKCS1v15(rand.Reader, r.privkey, ciphertext) 57 | if err != nil { 58 | return nil, err 59 | } 60 | return plaintext, nil 61 | } 62 | 63 | //nolint:revive,stylecheck 64 | type RSA_SHA256Signer struct { 65 | privkey *rsa.PrivateKey 66 | } 67 | 68 | //nolint:revive,stylecheck 69 | func NewRSA_SHA256Signer(privkey *rsa.PrivateKey) *RSA_SHA256Signer { 70 | r := &RSA_SHA256Signer{ 71 | privkey: privkey, 72 | } 73 | return r 74 | } 75 | 76 | func (r *RSA_SHA256Signer) Sign(data []byte) (signature []byte, err error) { 77 | if len(data) == 0 { 78 | return nil, errors.New("cannot sign empty byte slice") 79 | } 80 | 81 | hash := sha256.Sum256(data) 82 | signature, err = rsa.SignPKCS1v15(rand.Reader, r.privkey, crypto.SHA256, hash[:]) 83 | if err != nil { 84 | return nil, err 85 | } 86 | 87 | return signature, nil 88 | } 89 | 90 | //nolint:revive,stylecheck 91 | type RSA_SHA256SignatureVerifier struct { 92 | pubkey *rsa.PublicKey 93 | } 94 | 95 | //nolint:revive,stylecheck 96 | func NewRSA_SHA256SignatureVerifier(pubkey *rsa.PublicKey) *RSA_SHA256SignatureVerifier { 97 | r := &RSA_SHA256SignatureVerifier{ 98 | pubkey: pubkey, 99 | } 100 | return r 101 | } 102 | 103 | func (r *RSA_SHA256SignatureVerifier) Verify(text, signature []byte) (valid bool, err error) { 104 | hashed := sha256.Sum256(text) 105 | err = rsa.VerifyPKCS1v15(r.pubkey, crypto.SHA256, hashed[:], signature) 106 | if err != nil { 107 | return false, err 108 | } 109 | 110 | return true, nil 111 | } 112 | 113 | func RSAKeypair(bitSize int) (*rsa.PrivateKey, *rsa.PublicKey, error) { 114 | key, err := rsa.GenerateKey(rand.Reader, bitSize) 115 | if err != nil { 116 | return nil, nil, err 117 | } 118 | pub, ok := key.Public().(*rsa.PublicKey) 119 | if !ok { 120 | return nil, nil, errors.New("type assertion failed") 121 | } 122 | 123 | return key, pub, nil 124 | } 125 | 126 | func RSAEncodePrivateKey(key *rsa.PrivateKey) (string, error) { 127 | p := pem.Block{ 128 | Type: "RSA PRIVATE KEY", 129 | Headers: nil, 130 | Bytes: x509.MarshalPKCS1PrivateKey(key), 131 | } 132 | 133 | var buf bytes.Buffer 134 | if err := pem.Encode(&buf, &p); err != nil { 135 | return "", errors.Wrap(err, "pem encode") 136 | } 137 | 138 | return buf.String(), nil 139 | } 140 | 141 | func RSAEncodePublicKey(key *rsa.PublicKey) (string, error) { 142 | pubKeyBytes, err := x509.MarshalPKIXPublicKey(key) 143 | if err != nil { 144 | return "", errors.Wrap(err, "extract key to DER format") 145 | } 146 | 147 | p := pem.Block{ 148 | Type: "PUBLIC KEY", 149 | Headers: nil, 150 | Bytes: pubKeyBytes, 151 | } 152 | 153 | var buf bytes.Buffer 154 | if err := pem.Encode(&buf, &p); err != nil { 155 | return "", errors.Wrap(err, "pem encode") 156 | } 157 | 158 | return buf.String(), nil 159 | } 160 | 161 | func RSADecodePrivateKey(key string) (*rsa.PrivateKey, error) { 162 | block, _ := pem.Decode([]byte(key)) 163 | if block == nil { 164 | return nil, errors.New("pem decode") 165 | } 166 | 167 | if block.Type != "RSA PRIVATE KEY" { 168 | return nil, errors.New("header is not RSA PRIVATE KEY") 169 | } 170 | 171 | pub, err := x509.ParsePKCS1PrivateKey(block.Bytes) 172 | if err != nil { 173 | return nil, errors.Wrap(err, "parse pkcs1 private key") 174 | } 175 | 176 | return pub, nil 177 | } 178 | 179 | func RSADecodePublicKey(key string) (*rsa.PublicKey, error) { 180 | block, _ := pem.Decode([]byte(key)) 181 | 182 | if block == nil { 183 | return nil, errors.New("pem decode") 184 | } 185 | 186 | if block.Type == "PUBLIC KEY" { 187 | pub, err := x509.ParsePKIXPublicKey(block.Bytes) 188 | if err != nil { 189 | return nil, errors.Wrap(err, "x509 parse pkix public key") 190 | } 191 | 192 | switch pub := pub.(type) { 193 | case *rsa.PublicKey: 194 | return pub, nil // This is what we are after 195 | 196 | // case *dsa.PublicKey: 197 | // return nil, errors.New("dsa public key") 198 | 199 | case *ecdsa.PublicKey: 200 | return nil, errors.New("ecdsa public key") 201 | } 202 | 203 | return nil, errors.New("unknown public key") 204 | } 205 | 206 | if block.Type == "RSA PUBLIC KEY" { 207 | pub, err := x509.ParsePKCS1PublicKey(block.Bytes) 208 | if err != nil { 209 | return nil, errors.Wrap(err, "x509 parse pkcs1 public key") 210 | } 211 | 212 | return pub, nil 213 | } 214 | 215 | return nil, errors.New("decode error") 216 | } 217 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/rsa_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRSAEncryptor_Encrypt(t *testing.T) { 11 | _, pub, err := RSAKeypair(2048) 12 | assert.NoError(t, err) 13 | r := NewRSAEncrypter(pub) 14 | 15 | plaintext := []byte("Hello world!") 16 | 17 | cipher, err := r.Encrypt(plaintext) 18 | assert.NoError(t, err) 19 | assert.NotNil(t, cipher) 20 | assert.NotEmpty(t, cipher) 21 | assert.Greater(t, len(cipher), len(plaintext)) 22 | assert.Len(t, cipher, 2048/8) 23 | } 24 | 25 | func TestRSAEncryptor_Decrypt(t *testing.T) { 26 | priv, pub, err := RSAKeypair(2048) 27 | assert.NoError(t, err) 28 | re := NewRSAEncrypter(pub) 29 | rd := NewRSADecrypter(priv) 30 | 31 | plaintext := []byte("Hello world!") 32 | 33 | cipher, err := re.Encrypt(plaintext) 34 | require.NoError(t, err) 35 | 36 | plain, err := rd.Decrypt(cipher) 37 | assert.NoError(t, err) 38 | assert.Equal(t, plaintext, plain) 39 | } 40 | 41 | func TestRSAEncryptor_Decrypt2(t *testing.T) { 42 | priv, pub, err := RSAKeypair(2048) 43 | assert.NoError(t, err) 44 | re := NewRSAEncrypter(pub) 45 | rd := NewRSADecrypter(priv) 46 | 47 | plaintext := []byte("Hello world!") 48 | 49 | cipher, err := re.Encrypt(plaintext) 50 | require.NoError(t, err) 51 | 52 | plain, err := rd.Decrypt(cipher) 53 | assert.NoError(t, err) 54 | assert.Equal(t, plaintext, plain) 55 | } 56 | 57 | func TestRSA_SHA256Signor(t *testing.T) { 58 | priv, _, err := RSAKeypair(2048) 59 | assert.NoError(t, err) 60 | s := NewRSA_SHA256Signer(priv) 61 | 62 | plaintext := []byte("Hello world!") 63 | signature, err := s.Sign(plaintext) 64 | assert.NoError(t, err) 65 | 66 | assert.NotNil(t, signature) 67 | assert.NotEmpty(t, signature) 68 | assert.Greater(t, len(signature), len(plaintext)) 69 | assert.Len(t, signature, 2048/8) 70 | } 71 | 72 | func TestRSA_SHA256SignatureVerifier(t *testing.T) { 73 | priv, pub, err := RSAKeypair(2048) 74 | assert.NoError(t, err) 75 | rs := NewRSA_SHA256Signer(priv) 76 | rv := NewRSA_SHA256SignatureVerifier(pub) 77 | 78 | plaintext := []byte("Hello world!") 79 | signature, err := rs.Sign(plaintext) 80 | require.NoError(t, err) 81 | 82 | ok, err := rv.Verify(plaintext, signature) 83 | assert.NoError(t, err) 84 | assert.True(t, ok) 85 | } 86 | 87 | func TestRSA_SHA256SignatureVerifier_FalseSignature(t *testing.T) { 88 | priv, pub, err := RSAKeypair(2048) 89 | assert.NoError(t, err) 90 | rs := NewRSA_SHA256Signer(priv) 91 | rv := NewRSA_SHA256SignatureVerifier(pub) 92 | 93 | plaintext := []byte("Hello world!") 94 | signature, err := rs.Sign(plaintext) 95 | require.NoError(t, err) 96 | 97 | ok, err := rv.Verify(plaintext, append(signature, 0x00)) 98 | assert.Error(t, err) 99 | assert.False(t, ok) 100 | } 101 | 102 | func TestRSA_SHA256SignatureVerifier_DifferentPlaintext(t *testing.T) { 103 | priv, pub, err := RSAKeypair(2048) 104 | assert.NoError(t, err) 105 | rs := NewRSA_SHA256Signer(priv) 106 | rv := NewRSA_SHA256SignatureVerifier(pub) 107 | 108 | plaintext := []byte("Hello world!") 109 | signature, err := rs.Sign(plaintext) 110 | require.NoError(t, err) 111 | 112 | ok, err := rv.Verify(plaintext[:len(plaintext)-1], signature) 113 | assert.Error(t, err) 114 | assert.False(t, ok) 115 | } 116 | 117 | func TestRSAEncodeDecode(t *testing.T) { 118 | priv, pub, err := RSAKeypair(2048) 119 | assert.NoError(t, err) 120 | 121 | privEnc, err := RSAEncodePrivateKey(priv) 122 | assert.NoError(t, err) 123 | 124 | privDec, err := RSADecodePrivateKey(privEnc) 125 | assert.NoError(t, err) 126 | require.NotNil(t, privDec) 127 | 128 | assert.True(t, privDec.Equal(priv)) 129 | 130 | pubEnc, err := RSAEncodePublicKey(pub) 131 | assert.NoError(t, err) 132 | pubDec, err := RSADecodePublicKey(pubEnc) 133 | assert.NoError(t, err) 134 | require.NotNil(t, pubDec) 135 | 136 | assert.True(t, pubDec.Equal(pub)) 137 | } 138 | 139 | func BenchmarkRSADecrypter_ActualCipher(b *testing.B) { 140 | priv, pub, err := RSAKeypair(2048) 141 | assert.NoError(b, err) 142 | re := NewRSAEncrypter(pub) 143 | rd := NewRSADecrypter(priv) 144 | 145 | plaintext := []byte("Hello world!") 146 | 147 | cipher, err := re.Encrypt(plaintext) 148 | require.NoError(b, err) 149 | 150 | b.ResetTimer() 151 | for i := 0; i < b.N; i++ { 152 | _, _ = rd.Decrypt(cipher) 153 | } 154 | } 155 | 156 | func BenchmarkRSADecrypter_2Bytes(b *testing.B) { 157 | priv, _, err := RSAKeypair(2048) 158 | assert.NoError(b, err) 159 | rd := NewRSADecrypter(priv) 160 | 161 | b.ResetTimer() 162 | for i := 0; i < b.N; i++ { 163 | _, _ = rd.Decrypt([]byte{0xef, 0xfe}) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pkg/openspalib/crypto/tlv.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | // Encrypted TLV8 Definition Keys 4 | const ( 5 | EncryptedPayloadKey = 1 6 | EncryptedSessionKey = 2 7 | ) 8 | 9 | // Encrypted Payload TLV8 Definition Keys 10 | const ( 11 | PacketKey = 1 12 | SignatureKey = 2 13 | NonceKey = 3 14 | ) 15 | -------------------------------------------------------------------------------- /pkg/openspalib/entries.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "encoding/binary" 5 | "math" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "github.com/pkg/errors" 11 | uuid "github.com/satori/go.uuid" 12 | ) 13 | 14 | const ( 15 | TimestampSize = 8 16 | TargetProtocolSize = 1 17 | TargetPortSize = 2 18 | IPV4Size = 4 19 | IPV6Size = 16 20 | DurationSize = 3 21 | ClientUUIDSize = 16 22 | ) 23 | 24 | func TimestampEncode(t time.Time) ([]byte, error) { 25 | b := make([]byte, 8) 26 | i := t.Unix() 27 | binary.BigEndian.PutUint64(b, uint64(i)) 28 | return b, nil 29 | } 30 | 31 | func TimestampDecode(b []byte) (time.Time, error) { 32 | if len(b) != TimestampSize { 33 | return time.Time{}, ErrInvalidBytes 34 | } 35 | 36 | i := binary.BigEndian.Uint64(b) 37 | t := time.Unix(int64(i), 0) 38 | 39 | return t.UTC(), nil 40 | } 41 | 42 | func TargetProtocolEncode(p InternetProtocolNumber) (byte, error) { 43 | return p.ToBin(), nil 44 | } 45 | 46 | func TargetProtocolDecode(b []byte) (InternetProtocolNumber, error) { 47 | if len(b) != TargetProtocolSize { 48 | return ProtocolUndefined, ErrInvalidBytes 49 | } 50 | 51 | return InternetProtocolFromNumber(b[0]) 52 | } 53 | 54 | func TargetPortStartEncode(p int) ([]byte, error) { 55 | if err := convertableToUint16(p); err != nil { 56 | return nil, errors.Wrap(err, "uint16 conversion") 57 | } 58 | return uint16Encode(uint16(p)) 59 | } 60 | func TargetPortEndEncode(p int) ([]byte, error) { 61 | return TargetPortStartEncode(p) 62 | } 63 | 64 | func convertableToUint16(i int) error { 65 | if i < 0 { 66 | return errors.Wrap(ErrBadInput, "cannot convert negative integers") 67 | } 68 | 69 | if math.Log2(float64(i)) >= 16 { 70 | return errors.Wrap(ErrBadInput, "too large for uint16") 71 | } 72 | return nil 73 | } 74 | 75 | func uint16Encode(i uint16) ([]byte, error) { 76 | b := make([]byte, 2) 77 | binary.BigEndian.PutUint16(b, i) 78 | return b, nil 79 | } 80 | 81 | func uint16Decode(b []byte) (uint16, error) { 82 | const size = 2 // bytes 83 | 84 | if len(b) > size || len(b) == 0 { 85 | return 0, ErrInvalidBytes 86 | } 87 | 88 | if len(b) == 1 { 89 | bTemp := make([]byte, 2) 90 | bTemp[0] = 0 91 | bTemp[1] = b[0] 92 | b = bTemp 93 | } 94 | 95 | return binary.BigEndian.Uint16(b), nil 96 | } 97 | 98 | func TargetPortStartDecode(b []byte) (int, error) { 99 | i, err := uint16Decode(b) 100 | return int(i), err 101 | } 102 | 103 | func TargetPortEndDecode(b []byte) (int, error) { 104 | return TargetPortStartDecode(b) 105 | } 106 | 107 | func IPv4Encode(ip net.IP) ([]byte, error) { 108 | ip = ip.To4() 109 | if !isIPv4(ip) || ip == nil { 110 | return nil, errors.Wrap(ErrBadInput, "input is not ipv4 address") 111 | } 112 | b := []byte(ip) 113 | return b, nil 114 | } 115 | 116 | func IPv4Decode(b []byte) (net.IP, error) { 117 | if len(b) != IPV4Size { 118 | return nil, ErrInvalidBytes 119 | } 120 | 121 | ip := net.IP(b) 122 | 123 | ip = ip.To4() 124 | if !isIPv4(ip) { 125 | return nil, errors.Wrap(ErrBadInput, "input is not ipv4 address") 126 | } 127 | 128 | return ip, nil 129 | } 130 | 131 | func IPv6Encode(ip net.IP) ([]byte, error) { 132 | ip = ip.To16() 133 | if !isIPv6(ip) || ip == nil { 134 | return nil, errors.Wrap(ErrBadInput, "input is not ipv6 address") 135 | } 136 | b := []byte(ip) 137 | return b, nil 138 | } 139 | 140 | func IPv6Decode(b []byte) (net.IP, error) { 141 | if len(b) != IPV6Size { 142 | return nil, ErrInvalidBytes 143 | } 144 | 145 | ip := net.IP(b) 146 | ip = ip.To16() 147 | if !isIPv6(ip) { 148 | return nil, errors.Wrap(ErrBadInput, "input is not ipv6 address") 149 | } 150 | 151 | return ip, nil 152 | } 153 | 154 | func isIPv4(ip net.IP) bool { 155 | return !isIPv6(ip) 156 | } 157 | 158 | func isIPv6(ip net.IP) bool { 159 | return strings.Contains(ip.String(), ":") 160 | } 161 | 162 | var DurationMax = int(math.Pow(2, 8*DurationSize)) - 1 163 | 164 | func DurationEncode(d time.Duration) ([]byte, error) { 165 | s := int(d.Seconds()) 166 | if s > DurationMax { 167 | return nil, errors.Wrap(ErrBadInput, "duration too long") 168 | } 169 | 170 | if s < 1 { 171 | return nil, errors.Wrap(ErrBadInput, "duration too small") 172 | } 173 | 174 | b := make([]byte, 4) 175 | binary.BigEndian.PutUint32(b, uint32(s)) 176 | 177 | return []byte{b[1], b[2], b[3]}, nil 178 | } 179 | 180 | func DurationDecode(b []byte) (time.Duration, error) { 181 | if len(b) != DurationSize { 182 | return time.Duration(0), ErrInvalidBytes 183 | } 184 | 185 | bCpy := make([]byte, 4) 186 | bCpy[0] = 0 187 | bCpy[1] = b[0] 188 | bCpy[2] = b[1] 189 | bCpy[3] = b[2] 190 | 191 | i := binary.BigEndian.Uint32(bCpy) 192 | 193 | return time.Second * time.Duration(i), nil 194 | } 195 | 196 | func ClientUUIDEncode(u string) ([]byte, error) { 197 | id, err := uuid.FromString(u) 198 | if err != nil { 199 | return nil, errors.Wrap(err, "uuid decode") 200 | } 201 | 202 | return id.Bytes(), nil 203 | } 204 | 205 | func ClientUUIDDecode(b []byte) (string, error) { 206 | if len(b) != ClientUUIDSize { 207 | return "", ErrInvalidBytes 208 | } 209 | 210 | u, err := uuid.FromBytes(b) 211 | if err != nil { 212 | return "", errors.Wrap(err, "uuid decode") 213 | } 214 | 215 | return u.String(), nil 216 | } 217 | -------------------------------------------------------------------------------- /pkg/openspalib/header.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/binary" 6 | 7 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 8 | errors "github.com/pkg/errors" 9 | ) 10 | 11 | const ( 12 | HeaderLength = 8 13 | ADKLength = 4 14 | ProtocolVersion = 2 15 | ) 16 | 17 | type PDUType string 18 | 19 | const ( 20 | RequestPDU PDUType = "request" 21 | ResponsePDU PDUType = "response" 22 | ) 23 | 24 | type Header struct { 25 | Type PDUType 26 | Version int 27 | TransactionID uint8 28 | CipherSuiteID crypto.CipherSuiteID 29 | ADKProof uint32 30 | } 31 | 32 | func NewHeader(t PDUType, c crypto.CipherSuiteID) Header { 33 | return Header{ 34 | Type: t, 35 | Version: ProtocolVersion, 36 | TransactionID: 0, 37 | CipherSuiteID: c, 38 | } 39 | } 40 | 41 | func (h *Header) Marshal() ([]byte, error) { 42 | b := make([]byte, HeaderLength-ADKLength, HeaderLength) 43 | b[0x00] = h.marshalControlField() 44 | b[0x01] = h.marshalTransactionID() 45 | b[0x02] = h.marshalCipherSuite() 46 | b[0x03] = 0 // reserved field 47 | 48 | b = append(b, h.marshalADKProof()...) 49 | return b, nil 50 | } 51 | 52 | func (h *Header) marshalControlField() byte { 53 | // T | Version | Reserved 54 | // T = PDU Type (1 bit) 55 | // Version = Protocol version (3 bits) 56 | // Reserved = Reserved for future use, should be all 0 (4 bits) 57 | 58 | // higher nibble 59 | b := uint8(h.Version) << 4 60 | 61 | if h.Type == ResponsePDU { 62 | b |= 0b1000_0000 63 | } else { 64 | // response, make sure bit 7 is 0 65 | b &= 0b0111_1111 66 | } 67 | 68 | // lower nibble is reserved 69 | 70 | return b 71 | } 72 | 73 | func (h *Header) marshalTransactionID() byte { 74 | return h.TransactionID 75 | } 76 | 77 | func (h *Header) marshalCipherSuite() byte { 78 | return byte(h.CipherSuiteID) 79 | } 80 | 81 | func (h *Header) marshalADKProof() []byte { 82 | b := make([]byte, 4) 83 | binary.BigEndian.PutUint32(b, h.ADKProof) 84 | return b 85 | } 86 | 87 | func (h *Header) unmarshalControlField(b byte) (t PDUType, version int) { 88 | t = RequestPDU 89 | 90 | if b>>7&0x01 == 1 { 91 | t = ResponsePDU 92 | } 93 | 94 | version = int((b >> 4) & 0x03) 95 | return 96 | } 97 | 98 | func (h *Header) unmarshalTransactionID(b byte) uint8 { 99 | return b 100 | } 101 | 102 | func (h *Header) unmarshalCipherSuite(b byte) crypto.CipherSuiteID { 103 | return crypto.CipherSuiteID(b) 104 | } 105 | 106 | func (h *Header) unmarshalADKProof(b []byte) (uint32, error) { 107 | if len(b) != ADKLength { 108 | return 0, errors.New("invalid length") 109 | } 110 | 111 | return binary.BigEndian.Uint32(b), nil 112 | } 113 | 114 | func UnmarshalHeader(b []byte) (Header, error) { 115 | if len(b) != HeaderLength { 116 | return Header{}, errors.New("invalid header length") 117 | } 118 | 119 | h := Header{} 120 | h.Type, h.Version = h.unmarshalControlField(b[0]) 121 | h.TransactionID = h.unmarshalTransactionID(b[1]) 122 | h.CipherSuiteID = h.unmarshalCipherSuite(b[2]) 123 | // b[3] is reserved field 124 | proof, err := h.unmarshalADKProof(b[4:HeaderLength]) 125 | if err != nil { 126 | return Header{}, errors.Wrap(err, "unmarshal adk proof") 127 | } 128 | h.ADKProof = proof 129 | 130 | return h, nil 131 | } 132 | 133 | func RandomTransactionID() uint8 { 134 | b := make([]byte, 1) 135 | _, err := rand.Read(b) 136 | if err != nil { 137 | panic(err) 138 | } 139 | return b[0] 140 | } 141 | -------------------------------------------------------------------------------- /pkg/openspalib/openspalib_test.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestOpenSpaLib_Usability(t *testing.T) { 14 | cs := crypto.NewCipherSuiteStub() 15 | clientUUID := RandomUUID() 16 | 17 | // Client: Create request 18 | r, err := NewRequest(RequestData{ 19 | TransactionID: 42, 20 | ClientUUID: clientUUID, 21 | ClientIP: net.IPv4(88, 200, 23, 30), 22 | TargetProtocol: ProtocolIPV4, 23 | TargetIP: net.IPv4(88, 200, 23, 40), 24 | TargetPortStart: 80, 25 | TargetPortEnd: 100, 26 | }, cs, RequestDataOpt{}) 27 | require.NoError(t, err) 28 | reqBytes, err := r.Marshal() 29 | require.NoError(t, err) 30 | 31 | // Server: Receive request 32 | rS, err := RequestUnmarshal(reqBytes, cs) 33 | require.NotNil(t, rS) 34 | assert.Equal(t, 2, rS.Header.Version) 35 | assert.Equal(t, uint8(42), rS.Header.TransactionID) 36 | require.NoError(t, err) 37 | 38 | firewallC, err := TLVFromContainer(rS.Body, FirewallKey) 39 | assert.NoError(t, err) 40 | assert.NotNil(t, firewallC) 41 | 42 | proto, err := TargetProtocolFromContainer(firewallC) 43 | assert.NoError(t, err) 44 | assert.Equal(t, ProtocolIPV4, proto) 45 | 46 | portA, err := TargetPortStartFromContainer(firewallC) 47 | assert.NoError(t, err) 48 | assert.Equal(t, 80, portA) 49 | 50 | portB, err := TargetPortEndFromContainer(firewallC) 51 | assert.NoError(t, err) 52 | assert.Equal(t, 100, portB) 53 | 54 | cIP, err := ClientIPFromContainer(firewallC) 55 | assert.NoError(t, err) 56 | assert.True(t, net.IPv4(88, 200, 23, 30).Equal(cIP)) 57 | 58 | sIP, err := TargetIPFromContainer(firewallC) 59 | assert.NoError(t, err) 60 | assert.True(t, net.IPv4(88, 200, 23, 40).Equal(sIP)) 61 | 62 | // Server: Send response 63 | resp, err := NewResponse(ResponseData{ 64 | TransactionID: 42, 65 | TargetProtocol: ProtocolIPV4, 66 | TargetIP: net.IPv4(88, 200, 23, 8), 67 | TargetPortStart: 80, 68 | TargetPortEnd: 100, 69 | Duration: 3 * time.Second, 70 | ClientUUID: clientUUID, 71 | }, cs) 72 | require.NoError(t, err) 73 | require.NotNil(t, resp) 74 | respBytes, err := resp.Marshal() 75 | assert.NoError(t, err) 76 | 77 | // Client: Receive response 78 | respC, err := ResponseUnmarshal(respBytes, cs) 79 | assert.NoError(t, err) 80 | require.NotNil(t, respC) 81 | assert.Equal(t, 2, respC.Header.Version) 82 | assert.Equal(t, uint8(42), respC.Header.TransactionID) 83 | 84 | firewallS, err := TLVFromContainer(respC.Body, FirewallKey) 85 | assert.NoError(t, err) 86 | assert.NotNil(t, firewallS) 87 | 88 | proto, err = TargetProtocolFromContainer(firewallS) 89 | assert.NoError(t, err) 90 | assert.Equal(t, ProtocolIPV4, proto) 91 | 92 | tIP, err := TargetIPFromContainer(firewallS) 93 | assert.NoError(t, err) 94 | assert.True(t, net.IPv4(88, 200, 23, 8).Equal(tIP)) 95 | 96 | portA, err = TargetPortStartFromContainer(firewallS) 97 | assert.NoError(t, err) 98 | assert.Equal(t, 80, portA) 99 | 100 | portB, err = TargetPortEndFromContainer(firewallS) 101 | assert.NoError(t, err) 102 | assert.Equal(t, 100, portB) 103 | 104 | d, err := DurationFromContainer(firewallS) 105 | assert.NoError(t, err) 106 | assert.Equal(t, 3*time.Second, d) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/openspalib/response.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "bytes" 5 | "net" 6 | "time" 7 | 8 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 9 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type ResponseData struct { 14 | TransactionID uint8 15 | 16 | ClientUUID string 17 | 18 | TargetProtocol InternetProtocolNumber 19 | TargetIP net.IP 20 | TargetPortStart int 21 | TargetPortEnd int 22 | 23 | Duration time.Duration 24 | } 25 | 26 | type ResponseExtendedData struct { 27 | } 28 | 29 | type Response struct { 30 | c crypto.CipherSuite 31 | 32 | Header Header 33 | Body tlv.Container 34 | 35 | // Metadata is not actually sent, but it is passed to the CipherSuite implementation, so we can provide additional 36 | // data that can be used by CipherSuite implementation for security purposes. This data is not packed into OpenSPA 37 | // request/responses, it is merely passed along various subsystems. 38 | Metadata tlv.Container 39 | } 40 | 41 | func NewResponse(d ResponseData, c crypto.CipherSuite) (*Response, error) { 42 | if c == nil { 43 | return nil, ErrCipherSuiteRequired 44 | } 45 | 46 | r := &Response{} 47 | r.c = c 48 | 49 | r.Header = NewHeader(ResponsePDU, c.CipherSuiteID()) 50 | r.Header.TransactionID = d.TransactionID 51 | 52 | ed, err := r.generateExtendedData() 53 | if err != nil { 54 | return nil, errors.Wrap(err, "response extended data generation") 55 | } 56 | 57 | r.Body = tlv.NewContainer() 58 | 59 | if err = r.bodyCreate(r.Body, d, ed); err != nil { 60 | return nil, errors.Wrap(err, "body create") 61 | } 62 | 63 | r.Metadata = tlv.NewContainer() 64 | if err := r.metadataCreate(r.Metadata, d); err != nil { 65 | return nil, errors.Wrap(err, "metadata create") 66 | } 67 | 68 | return r, nil 69 | } 70 | 71 | func (r *Response) Marshal() ([]byte, error) { 72 | header, err := r.Header.Marshal() 73 | if err != nil { 74 | return nil, errors.Wrap(err, "header marshal") 75 | } 76 | 77 | ec, err := r.c.Secure(header, r.Body, r.Metadata) 78 | if err != nil { 79 | return nil, errors.Wrap(err, "secure") 80 | } 81 | 82 | buf := bytes.Buffer{} 83 | buf.Write(header) 84 | buf.Write(ec.Bytes()) 85 | 86 | b := buf.Bytes() 87 | 88 | if len(b) > MaxPDUSize { 89 | return b, ErrPDUTooLarge 90 | } 91 | 92 | return b, nil 93 | } 94 | 95 | func (r *Response) generateExtendedData() (ResponseExtendedData, error) { 96 | ed := ResponseExtendedData{} 97 | 98 | return ed, nil 99 | } 100 | 101 | func (r *Response) bodyCreate(c tlv.Container, d ResponseData, ed ResponseExtendedData) error { 102 | firewallC := tlv.NewContainer() 103 | 104 | if err := TargetProtocolToContainer(firewallC, d.TargetProtocol); err != nil { 105 | return errors.Wrap(err, "protocol to container") 106 | } 107 | 108 | if isIPv4(d.TargetIP) { 109 | if err := TargetIPv4ToContainer(firewallC, d.TargetIP); err != nil { 110 | return errors.Wrap(err, "target ipv4 to container") 111 | } 112 | } else { 113 | if err := TargetIPv6ToContainer(firewallC, d.TargetIP); err != nil { 114 | return errors.Wrap(err, "target ipv6 to container") 115 | } 116 | } 117 | 118 | if err := TargetPortStartToContainer(firewallC, d.TargetPortStart); err != nil { 119 | return errors.Wrap(err, "port start to container") 120 | } 121 | 122 | if err := TargetPortEndToContainer(firewallC, d.TargetPortEnd); err != nil { 123 | return errors.Wrap(err, "port end to container") 124 | } 125 | 126 | if err := DurationToContainer(firewallC, d.Duration); err != nil { 127 | return errors.Wrap(err, "duration to container") 128 | } 129 | 130 | if err := TLVToContainer(c, firewallC, FirewallKey); err != nil { 131 | return errors.Wrap(err, "firewall tlv8 container to container") 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (r *Response) metadataCreate(c tlv.Container, d ResponseData) error { 138 | if err := ClientUUIDToContainer(c, d.ClientUUID); err != nil { 139 | return errors.Wrap(err, "client uuid to container") 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func ResponseUnmarshal(b []byte, cs crypto.CipherSuite) (*Response, error) { 146 | if len(b) < HeaderLength { 147 | return nil, errors.New("too short to be response") 148 | } 149 | 150 | headerB := b[:HeaderLength] 151 | 152 | header, err := UnmarshalHeader(headerB) 153 | if err != nil { 154 | return nil, errors.Wrap(err, "unmarshal header") 155 | } 156 | 157 | c, err := tlv.UnmarshalTLVContainer(b[HeaderLength:]) 158 | if err != nil { 159 | return nil, errors.Wrap(err, "unmarshal tlv container") 160 | } 161 | 162 | body, err := cs.Unlock(headerB, c) 163 | if err != nil { 164 | return nil, errors.Wrap(err, "crypto unlock") 165 | } 166 | 167 | r := &Response{ 168 | c: cs, 169 | Header: header, 170 | Body: body, 171 | } 172 | 173 | return r, nil 174 | } 175 | -------------------------------------------------------------------------------- /pkg/openspalib/response_test.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | "time" 7 | 8 | "github.com/greenstatic/openspa/pkg/openspalib/crypto" 9 | "github.com/greenstatic/openspa/pkg/openspalib/tlv" 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/mock" 12 | ) 13 | 14 | func TestNewResponse(t *testing.T) { 15 | cs := crypto.NewCipherSuiteStub() 16 | clientUUID := RandomUUID() 17 | dur := 3 * time.Hour 18 | 19 | tIP := net.IPv4(88, 200, 23, 19) 20 | r, err := NewResponse(ResponseData{ 21 | TransactionID: 123, 22 | TargetProtocol: ProtocolIPV4, 23 | TargetIP: tIP, 24 | TargetPortStart: 80, 25 | TargetPortEnd: 120, 26 | Duration: dur, 27 | ClientUUID: clientUUID, 28 | }, cs) 29 | 30 | assert.NoError(t, err) 31 | assert.NotNil(t, r) 32 | 33 | assert.Equal(t, byte(123), r.Header.TransactionID) 34 | assert.Equal(t, uint32(0), r.Header.ADKProof) 35 | 36 | firewallC, err := TLVFromContainer(r.Body, FirewallKey) 37 | assert.NoError(t, err) 38 | assert.NotNil(t, firewallC) 39 | assert.NotEqual(t, 0, firewallC.NoEntries()) 40 | 41 | p, err := TargetProtocolFromContainer(firewallC) 42 | assert.NoError(t, err) 43 | assert.Equal(t, ProtocolIPV4, p) 44 | 45 | ip, err := TargetIPFromContainer(firewallC) 46 | assert.NoError(t, err) 47 | assert.True(t, tIP.Equal(ip)) 48 | 49 | ps, err := TargetPortStartFromContainer(firewallC) 50 | assert.NoError(t, err) 51 | assert.Equal(t, 80, ps) 52 | 53 | pe, err := TargetPortEndFromContainer(firewallC) 54 | assert.NoError(t, err) 55 | assert.Equal(t, 120, pe) 56 | 57 | d, err := DurationFromContainer(firewallC) 58 | assert.NoError(t, err) 59 | assert.Equal(t, dur, d) 60 | 61 | uuid, err := ClientUUIDFromContainer(r.Metadata) 62 | assert.NoError(t, err) 63 | assert.Equal(t, clientUUID, uuid) 64 | } 65 | 66 | func TestResponseSize_Stub(t *testing.T) { 67 | cs := crypto.NewCipherSuiteStub() 68 | 69 | r, err := NewResponse(testResponseData(), cs) 70 | assert.NoError(t, err) 71 | assert.NotNil(t, r) 72 | 73 | b, err := r.Marshal() 74 | assert.Less(t, 0, len(b)) 75 | assert.NoError(t, err) 76 | 77 | t.Logf("Cipher=none test Response marshaled size: %d", len(b)) 78 | } 79 | 80 | func TestResponse_bodyCreate(t *testing.T) { 81 | c := tlv.NewContainer() 82 | r := Response{} 83 | rd := ResponseData{ 84 | TransactionID: RandomTransactionID(), 85 | ClientUUID: RandomUUID(), 86 | TargetProtocol: ProtocolTCP, 87 | TargetIP: net.IPv4(88, 200, 23, 24), 88 | TargetPortStart: 80, 89 | TargetPortEnd: 2000, 90 | Duration: time.Hour, 91 | } 92 | 93 | ed, err := r.generateExtendedData() 94 | assert.NoError(t, err) 95 | assert.NoError(t, r.bodyCreate(c, rd, ed)) 96 | 97 | firewallC, err := TLVFromContainer(c, FirewallKey) 98 | assert.NoError(t, err) 99 | assert.NotNil(t, firewallC) 100 | 101 | _, err = TargetProtocolFromContainer(firewallC) 102 | assert.NoError(t, err) 103 | 104 | _, err = TargetIPFromContainer(firewallC) 105 | assert.NoError(t, err) 106 | 107 | _, err = TargetPortStartFromContainer(firewallC) 108 | assert.NoError(t, err) 109 | 110 | _, err = TargetPortEndFromContainer(firewallC) 111 | assert.NoError(t, err) 112 | 113 | _, err = DurationFromContainer(firewallC) 114 | assert.NoError(t, err) 115 | } 116 | 117 | func TestResponse_metadataCreate(t *testing.T) { 118 | c := tlv.NewContainer() 119 | r := Response{} 120 | rd := ResponseData{ 121 | TransactionID: RandomTransactionID(), 122 | ClientUUID: RandomUUID(), 123 | TargetProtocol: ProtocolTCP, 124 | TargetIP: net.IPv4(88, 200, 23, 24), 125 | TargetPortStart: 80, 126 | TargetPortEnd: 2000, 127 | Duration: time.Hour, 128 | } 129 | assert.NoError(t, r.metadataCreate(c, rd)) 130 | 131 | _, err := ClientUUIDFromContainer(c) 132 | assert.NoError(t, err) 133 | } 134 | 135 | func TestResponseSize_RSA_SHA256_AES_256_CBC_with2048Keypair(t *testing.T) { 136 | key1, _, err := crypto.RSAKeypair(2048) 137 | assert.NoError(t, err) 138 | 139 | _, pub2, err := crypto.RSAKeypair(2048) 140 | assert.NoError(t, err) 141 | 142 | res := crypto.NewPublicKeyResolverMock() 143 | res.On("PublicKey", mock.Anything, mock.Anything).Return(pub2, nil) 144 | 145 | cs := crypto.NewCipherSuite_RSA_SHA256_AES256CBC(key1, res) 146 | 147 | r, err := NewResponse(testResponseData(), cs) 148 | assert.NoError(t, err) 149 | assert.NotNil(t, r) 150 | 151 | b, err := r.Marshal() 152 | assert.Less(t, 0, len(b)) 153 | assert.NoError(t, err) 154 | 155 | t.Logf("Cipher=RSA_SHA256_AES_256_CBC (2048 client and server keypair) test Response marshaled size: %d", len(b)) 156 | } 157 | 158 | func TestResponseSize_RSA_SHA256_AES_256_CBC_with4096Keypair(t *testing.T) { 159 | key1, _, err := crypto.RSAKeypair(4096) 160 | assert.NoError(t, err) 161 | 162 | _, pub2, err := crypto.RSAKeypair(4096) 163 | assert.NoError(t, err) 164 | 165 | res := crypto.NewPublicKeyResolverMock() 166 | res.On("PublicKey", mock.Anything, mock.Anything).Return(pub2, nil) 167 | 168 | cs := crypto.NewCipherSuite_RSA_SHA256_AES256CBC(key1, res) 169 | 170 | r, err := NewResponse(testResponseData(), cs) 171 | assert.NoError(t, err) 172 | assert.NotNil(t, r) 173 | 174 | b, err := r.Marshal() 175 | assert.Less(t, 0, len(b)) 176 | assert.NoError(t, err) 177 | 178 | t.Logf("Cipher=RSA_SHA256_AES_256_CBC (4096 client and server keypair) test Response marshaled size: %d", len(b)) 179 | } 180 | 181 | func testResponseData() ResponseData { 182 | return ResponseData{ 183 | TransactionID: 123, 184 | TargetProtocol: ProtocolIPV4, 185 | TargetIP: net.ParseIP("2001:1470:fffd:66::23:19"), 186 | TargetPortStart: 80, 187 | TargetPortEnd: 100, 188 | Duration: time.Hour, 189 | ClientUUID: RandomUUID(), 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /pkg/openspalib/tlv/README.md: -------------------------------------------------------------------------------- 1 | # TLV 2 | Binary encoding scheme based on the [Type-length-value](https://en.wikipedia.org/wiki/Type%E2%80%93length%E2%80%93value) 3 | scheme. 4 | The purpose of this is to strike a balance between expandability and space constraints due to OpenSPA's limited protocol 5 | payload size. 6 | 7 | Encoding scheme (a single TLV8 item): 8 | ``` 9 | | Type (1 byte) | Length (1 byte) | Value | 10 | ``` 11 | 12 | * Length: the length of the *Value* field 13 | * Value: binary data that is encoded dependent on the Type 14 | 15 | Encoding rules: 16 | * A length of 0 is valid, which means the Value portion of the field is skipped 17 | * Encoded values of length <= 255 (2^8) should fit into a single TLV8 item 18 | * Encoded values of length > 255 need to be fragmented 19 | * A fragmented item requires containing multiple sequential TLV8 items, each TLV8 item containing that fragmented items 20 | value length 21 | * A fragmented item requires all but the last item to be of length 255 22 | * Multiple non-fragmented TLV8 items with the same Type are allowed only if seperated by a TLV8 item of a different type (or the Type 23 | separator, see rule below) 24 | * Type 0x00 is a separator and has the implicit length of 0 25 | -------------------------------------------------------------------------------- /pkg/openspalib/tlv/item.go: -------------------------------------------------------------------------------- 1 | package tlv 2 | 3 | import "io" 4 | 5 | const ( 6 | itemLengthMax = 0xFF 7 | itemTypeSeparator uint8 = 0x00 8 | ) 9 | 10 | var ( 11 | itemSeparator = Item{ 12 | Type: itemTypeSeparator, 13 | Value: nil, 14 | } 15 | ) 16 | 17 | type Item struct { 18 | Type uint8 19 | Value []byte 20 | } 21 | 22 | func (item *Item) output(w io.Writer) error { 23 | if item.Type == itemTypeSeparator { 24 | // Type, Length & Value omitted 25 | return itemSeparatorOutput(w) 26 | } 27 | 28 | if len(item.Value) == 0 { 29 | // Type & Length=0, Value omitted 30 | _, err := w.Write([]byte{item.Type, 0x00}) 31 | return err 32 | } 33 | 34 | // Fragment the item if required 35 | for i := 0; i < len(item.Value); i += itemLengthMax { 36 | rem := len(item.Value) - i 37 | if rem > itemLengthMax { 38 | rem = itemLengthMax 39 | } 40 | 41 | b := item.Value[i : i+rem] 42 | _, err := w.Write([]byte{item.Type, uint8(len(b))}) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | _, err = w.Write(b) 48 | if err != nil { 49 | return err 50 | } 51 | } 52 | 53 | return nil 54 | } 55 | 56 | func itemSeparatorOutput(w io.Writer) error { 57 | _, err := w.Write([]byte{itemTypeSeparator}) 58 | return err 59 | } 60 | -------------------------------------------------------------------------------- /pkg/openspalib/tlv/item_test.go: -------------------------------------------------------------------------------- 1 | package tlv 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestItemOutput_ValueNil(t *testing.T) { 11 | i := Item{ 12 | Type: 0x1, 13 | Value: nil, 14 | } 15 | 16 | b := &bytes.Buffer{} 17 | assert.NoError(t, i.output(b)) 18 | 19 | assert.Equal(t, []byte{ 20 | 0x01, 21 | 0x00, 22 | }, b.Bytes()) 23 | } 24 | 25 | func TestItemOutput_ValueEmpty(t *testing.T) { 26 | i := Item{ 27 | Type: 0x1, 28 | Value: []byte{}, 29 | } 30 | 31 | b := &bytes.Buffer{} 32 | assert.NoError(t, i.output(b)) 33 | 34 | assert.Equal(t, []byte{ 35 | 0x01, 36 | 0x00, 37 | }, b.Bytes()) 38 | } 39 | 40 | func TestItemOutput_Type0(t *testing.T) { 41 | i := Item{ 42 | Type: 0x0, 43 | Value: []byte{1, 2, 3}, 44 | } 45 | 46 | b := &bytes.Buffer{} 47 | assert.NoError(t, i.output(b)) 48 | 49 | assert.Equal(t, []byte{ 50 | 0x00, 51 | }, b.Bytes()) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/openspalib/tlv/mocks_and_stubs.go: -------------------------------------------------------------------------------- 1 | package tlv 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/emirpasic/gods/maps" 9 | "github.com/emirpasic/gods/maps/hashmap" 10 | "github.com/stretchr/testify/mock" 11 | ) 12 | 13 | var _ Container = &ContainerMock{} 14 | 15 | type ContainerMock struct { 16 | mock.Mock 17 | } 18 | 19 | func NewContainerMock() *ContainerMock { 20 | c := &ContainerMock{} 21 | return c 22 | } 23 | 24 | func (c *ContainerMock) GetByte(key uint8) (b byte, exists bool) { 25 | args := c.Mock.Called(key) 26 | return args.Get(0).(byte), args.Bool(1) 27 | } 28 | 29 | func (c *ContainerMock) GetBytes(key uint8) (b []byte, exists bool) { 30 | args := c.Mock.Called(key) 31 | return args.Get(0).([]byte), args.Bool(1) 32 | } 33 | 34 | func (c *ContainerMock) SetByte(key uint8, value byte) { 35 | c.Mock.Called(key, value) 36 | } 37 | 38 | func (c *ContainerMock) SetBytes(key uint8, value []byte) { 39 | c.Mock.Called(key, value) 40 | } 41 | 42 | func (c *ContainerMock) Remove(key uint8) { 43 | c.Mock.Called(key) 44 | } 45 | 46 | func (c *ContainerMock) Bytes() []byte { 47 | args := c.Mock.Called() 48 | return args.Get(0).([]byte) 49 | } 50 | 51 | func (c *ContainerMock) Size() int { 52 | args := c.Mock.Called() 53 | return args.Int(0) 54 | } 55 | 56 | func (c *ContainerMock) NoEntries() int { 57 | args := c.Mock.Called() 58 | return args.Int(0) 59 | } 60 | 61 | var _ Container = &ContainerStub{} 62 | 63 | type ContainerStub struct { 64 | m maps.Map 65 | lock sync.Mutex 66 | } 67 | 68 | func NewContainerStub() *ContainerStub { 69 | c := &ContainerStub{} 70 | 71 | c.m = hashmap.New() 72 | 73 | return c 74 | } 75 | 76 | func (c *ContainerStub) GetByte(key uint8) (b byte, exists bool) { 77 | c.lock.Lock() 78 | defer c.lock.Unlock() 79 | 80 | v, ok := c.m.Get(key) 81 | if !ok { 82 | return 0, false 83 | } 84 | 85 | buf, ok := v.([]byte) 86 | if !ok { 87 | panic(fmt.Sprintf("Expected type []byte but received: %s", reflect.TypeOf(buf))) 88 | } 89 | 90 | return buf[0], true 91 | } 92 | 93 | func (c *ContainerStub) GetBytes(key uint8) (b []byte, exists bool) { 94 | c.lock.Lock() 95 | defer c.lock.Unlock() 96 | 97 | v, ok := c.m.Get(key) 98 | if !ok { 99 | return nil, false 100 | } 101 | 102 | buf, ok := v.([]byte) 103 | if !ok { 104 | panic(fmt.Sprintf("Expected type []byte but received: %s", reflect.TypeOf(buf))) 105 | } 106 | 107 | return buf, true 108 | } 109 | 110 | func (c *ContainerStub) SetByte(key uint8, value byte) { 111 | c.lock.Lock() 112 | defer c.lock.Unlock() 113 | 114 | c.m.Put(key, []byte{value}) 115 | } 116 | 117 | func (c *ContainerStub) SetBytes(key uint8, value []byte) { 118 | c.lock.Lock() 119 | defer c.lock.Unlock() 120 | 121 | c.m.Put(key, value) 122 | } 123 | 124 | func (c *ContainerStub) Remove(key uint8) { 125 | c.lock.Lock() 126 | defer c.lock.Unlock() 127 | 128 | c.m.Remove(key) 129 | } 130 | 131 | func (c *ContainerStub) Bytes() []byte { 132 | panic("not implemented") 133 | } 134 | 135 | func (c *ContainerStub) Size() int { 136 | return len(c.Bytes()) 137 | } 138 | 139 | func (c *ContainerStub) NoEntries() int { 140 | c.lock.Lock() 141 | defer c.lock.Unlock() 142 | 143 | return len(c.m.Values()) 144 | } 145 | -------------------------------------------------------------------------------- /pkg/openspalib/tlv/tlv.go: -------------------------------------------------------------------------------- 1 | package tlv 2 | 3 | type Container interface { 4 | GetByte(key uint8) (b byte, exists bool) 5 | GetBytes(key uint8) (b []byte, exists bool) 6 | 7 | SetByte(key uint8, value byte) 8 | SetBytes(key uint8, value []byte) 9 | 10 | Remove(key uint8) 11 | 12 | Bytes() []byte 13 | 14 | // Size returns the length of the byte slice or buffer 15 | // Size() int 16 | 17 | // NoEntries returns the number of nodes in the container 18 | NoEntries() int 19 | } 20 | -------------------------------------------------------------------------------- /pkg/openspalib/version.go: -------------------------------------------------------------------------------- 1 | package openspalib 2 | 3 | import "fmt" 4 | 5 | const VersionMajor = 2 6 | const VersionMinor = 0 7 | 8 | func Version() string { 9 | return fmt.Sprintf("%d.%d", VersionMajor, VersionMinor) 10 | } 11 | --------------------------------------------------------------------------------