Skip to content

Commit

Permalink
feat: add Word of Wisdom pow server and client
Browse files Browse the repository at this point in the history
  • Loading branch information
Nikita Kryuchkov committed Jan 13, 2022
1 parent 0c49136 commit a4bfb6d
Show file tree
Hide file tree
Showing 30 changed files with 1,847 additions and 6 deletions.
9 changes: 9 additions & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CLIENT_HOST=127.0.0.1
CLIENT_PORT=7092

SERVER_HOST=127.0.0.1
SERVER_PORT=8092

FILE_NAME=./cmd/server/quotes.txt

TCP_ADDRS=127.0.0.1:9092,127.0.0.1:9093,127.0.0.1:9094
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.build/
vendor/
vendor/
coverage.out
memprofile.out
24 changes: 24 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
FLAGS?=-v

lint:
golangci-lint run ./... --timeout 30m -v

test-race:
go test $(FLAGS) ./...--race -cover -test.timeout 5s -count 1

test:
go test $(FLAGS) ./... -cover -test.timeout 5s -count 1

client:
go build $(FLAGS) -race -o ./.build/client power/cmd/client

server:
go build $(FLAGS) -race -o ./.build/server power/cmd/server

gen:
go generate ./...

docker-run:
docker-compose build && docker-compose up

.PHONY: lint test test-race gen client server docker-run
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Introduction

Denial-of-Service-attacks are a typical situation when providing services over a network. A method for preventing DoS-attacks is to have the client show its dedication towards the service before gaining access to it. As proof of dedication, the client is requested to compute an answer to an algorithmic nonce.

The nonce should be hard to solve but the answer should be easy to verify. When the computation is done, the answer is sent to the server which verifies the answer. The nonce puts a heavy load on the client if several requests are made in a short time span; this prevents the client from abusing the service. This way of authentication for using a service is named a proof-of-work protocol. The client will have to provide proof of work (POW).

POW will not suffice as a guardian against DDoS. DoS protection is perhaps more essential because any client on the Internet is able to perform DoS attacks on their own. Having your service use an implementation of POW could not aid with handling DoS attacks if you use a protocol such as **TCP** (Transmission Control Protocol) because you are vulnerable to attacks from any single computer. Because before POW the client sends an SYN-packet to the server telling the server it wants to connect.

The server responds with an **SYN-ACK**. In the end, the client responds with an **ACK** back to the server. Only after this, a connection has been established and data can be sent. A typical DoS attack is simply to flood a server with **SYN**-packets but never respond to the servers **SYN-ACK**-packets. The server can have a limited number of outstanding SYN-ACKs and when the queue is full it will drop incoming **SYN**s.

# Implementation

This version of a POW that I implemented over **UDP** and **TCP** had the following characteristics:
• The protocol needs to scale well when more clients try to connect
• The workload on the server should be lower than on the client
• It should be impossible for a client to do any precalculations

## Reverse Computing a Hash

The pow-alogorith explained below is based on the reverse computation of hashed bytes from DOS-resistant authentication. Generally, it is almost impossible to reverse a hash to original bytes, however, it is easier to find a similar hash easy.

This is the key idea for this puzzle:
```
h(X) = 000 . . . 000 | {z } m zeros +Y
```

where h is a hash function, m ∈ Z.

The difficulty m specifies how many leading zeros the hash h should contain. The client then attempts to find an X that has a hash value with the set number of leading zero.

## The protocol
![protocol](https://www.planttext.com/api/plantuml/png/VP6x2iCm34LtVuL6P_0FX582NJjtDxQQ1Fm8bbB8tzSc8Qy-Di4zEbV63R5EF7edHZlSOWYWhf37UqySCDLbhk61gNzE4lt0KoMs6DHCbyK32hBJr5NYhxK4Q1WaHVT2MwtmHQcVj6GpWBOs8T7HduFv3fDGCu86Cw_qCOWbNBZLt29dpcUNRd65Il-U8Wps2tPo6HVfrBf_q7RU9zVaWlm7Rm00)

A request with a hash is sent to the server over **UDP** the protocol. The hash is a SHA256[32] hash made up of nonce. The server sends the nonce to the client, the client has to solve that when the solution sends back to the server. A common property of the hashes is that they have to be non-pre-computable. If a hash is pre-computable, a hacker could spend some time calculating solutions for a hash before an attack.

## How to check using Docker

```
make docker-run
```
55 changes: 55 additions & 0 deletions cmd/client/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"context"
"fmt"
"net"
"os"
"strconv"

"power/internal/config"
"power/internal/pow"
"power/internal/provider/power"

"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
log "github.com/sirupsen/logrus"
)

func main() {
powDebug, _ := strconv.ParseBool(os.Getenv("POW_DEBUG"))
if powDebug {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env: %s", err.Error())
}
log.SetLevel(log.DebugLevel)
}

var conf config.Config
err := envconfig.Process("server", &conf)
if err != nil {
log.Fatalf("Error loading config: %s", err.Error())
}

conn, err := net.ListenUDP("udp", getResolveUDPAddr(conf.ClientAddr()))
if err != nil {
log.Fatalf("couldn't open a udp connection: %s", conf.ServerAddr())
}
defer conn.Close()

ctx := context.Background()
client := power.New(ctx, conn, getResolveUDPAddr(conf.ServerAddr()), pow.SolveHash)
msg, err := client.GetMessage(ctx)
if err != nil {
log.Fatalf("couldn't read message from server %s", err.Error())
}

fmt.Println(string(msg))
client.Close()
}

func getResolveUDPAddr(address string) *net.UDPAddr {
t, _ := net.ResolveUDPAddr("udp", address)
return t
}
89 changes: 89 additions & 0 deletions cmd/server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package main

import (
"bytes"
"context"
"io/ioutil"
"math/rand"
"net"
"os"
"os/signal"
"strconv"
"time"

"power/internal/config"
"power/internal/protomsg"
"power/internal/services/server"

"github.com/apibillme/cache"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"
log "github.com/sirupsen/logrus"
)

const cacheTime = 2 * time.Second

func main() {
cache := cache.New(128, cache.WithTTL(cacheTime))
ctx, cancel := context.WithCancel(context.Background())

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
oscall := <-c
log.Printf("system call: %+v", oscall)
cancel()
}()

powDebug, _ := strconv.ParseBool(os.Getenv("POW_DEBUG"))
if powDebug {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env: %s", err.Error())
}
log.SetLevel(log.DebugLevel)
}

var conf config.Config
err := envconfig.Process("server", &conf)
if err != nil {
log.Fatalf("Error loading config: %s", err.Error())
}

quotesRaw, err := ioutil.ReadFile(conf.QuotesFileName)
if err != nil {
log.Fatalf("couldn't open a file with quotes %s", err.Error())
}

quotes := bytes.Split(quotesRaw, []byte("\n"))
if len(quotes) == 0 {
log.Fatal("quotes == 0")
}

handler := func() *protomsg.Message {
msg := &protomsg.Message{
Command: protomsg.CommandTypeMsg,
Body: []byte(quotes[rand.Intn(len(quotes))]),
}
return msg
}

rand.Seed(time.Now().UnixNano())
addrs := make([]*net.TCPAddr, 0, len(conf.TCPaddrs))
for i := range conf.TCPaddrs {
addrs = append(addrs, getResolveTCPAddr(conf.TCPaddrs[i]))
}
serv, err := server.NewUDP(conf.ServerHost, conf.ServerPort, cache, addrs, server.WithHandler(handler))
if err != nil {
log.Fatalf("couldn't create a new server instance %s", err.Error())
}

go serv.Listen(ctx)
<-ctx.Done()
log.Println("server exited properly")
}

func getResolveTCPAddr(address string) *net.TCPAddr {
t, _ := net.ResolveTCPAddr("tcp", address)
return t
}
2 changes: 2 additions & 0 deletions cmd/server/quotes.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
The souls of the righteous are in the hand of God\n and no torment shall touch them.
Don’t worry about the competition.\nOnly worry if you’re working in an area that’s not attracting any competition.
34 changes: 34 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
version: "3.9"
services:
server:
networks:
power-net:
ipv4_address: 10.5.0.5
build:
context: .
dockerfile: ./docker/Dockerfile.server
ports:
- "8092:8092"
- "9092:9092"
- "9093:9093"
- "9094:9094"
client:
depends_on:
server:
condition: service_started
networks:
power-net:
ipv4_address: 10.5.0.6
build:
context: .
dockerfile: ./docker/Dockerfile.client
ports:
- "7092:7092"

networks:
power-net:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.5.0.0/24
19 changes: 19 additions & 0 deletions docker/Dockerfile.client
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM golang:1.17

ADD ./ $GOPATH/src
WORKDIR $GOPATH/src

ENV GOBIN $GOPATH/bin
ENV GOSUMDB off
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV POW_DEBUG=false

ENV CLIENT_HOST=10.5.0.6
ENV CLIENT_PORT=7092
ENV SERVER_HOST=10.5.0.5
ENV SERVER_PORT=8092

RUN make test
RUN go build -o $GOBIN/client power/cmd/client
CMD ["client"]
19 changes: 19 additions & 0 deletions docker/Dockerfile.server
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM golang:1.17

ADD ./ $GOPATH/src
WORKDIR $GOPATH/src

ENV GOBIN $GOPATH/bin
ENV GOSUMDB off
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV POW_DEBUG=false

ENV SERVER_HOST=10.5.0.5
ENV SERVER_PORT=8092
ENV FILE_NAME=./cmd/server/quotes.txt
ENV TCP_ADDRS=10.5.0.5:9092,10.5.0.5:9093,10.5.0.5:9094

RUN make test
RUN go build -o $GOBIN/server power/cmd/server
CMD ["server"]
25 changes: 25 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module power

go 1.17

require (
github.com/apibillme/cache v0.0.0-20180927200649-e0b3581c9b4d
github.com/golang/mock v1.6.0
github.com/joho/godotenv v1.4.0
github.com/kelseyhightower/envconfig v1.4.0
github.com/kryuchkovnet/protobuf v0.0.0-00010101000000-000000000000
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.0
go.uber.org/goleak v1.1.12
google.golang.org/protobuf v1.27.1
)

replace github.com/kryuchkovnet/protobuf => ./protobuf/

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
Loading

0 comments on commit a4bfb6d

Please sign in to comment.