Design and implement some tcp server.
- TCP server should be protected from DDOS attacks with the Prof of Work (https://en.wikipedia.org/wiki/Proof_of_work), the challenge-response protocol should be used.
- The choice of the PoW algorithm should be explained.
- After Prof Of Work verification, server should send one of the quotes from "word of wisdom" book or any other collection of the quotes.
- Docker file should be provided both for the server and for the client that solves the PoW challenge.
Command | Description |
---|---|
help | Show command list |
lint | Run code linters |
test | Run unit-tests |
mock | Generate mocks |
build_server | Build a binary server |
build_client | Build a binary client |
startd | Compose run detached |
start | Compose run |
stop | Compose down |
log | Compose logs |
logserver | Compose server logs |
logclient | Compose client logs |
make start
RequestType | ResourceID | ContentLength | Payload |
---|---|---|---|
[1]bytes | [2]bytes | [4]bytes | [...]bytes |
Request type can take one of the following values:
0x00
: RequestTypeExit0x01
: RequestTypeChallenge0x02
: RequestTypeResource
An arbitrary resource identifier. Needed to determine which handler was called.
Request body length (Payload)
Optional request body
Status | ContentLength | Payload |
---|---|---|
[1]bytes | [4]bytes | [...]bytes |
Response status can take one of the following values:
0x00
: StatusOK0x01
: StatusErr
Response body length (Payload)
Optional response body
The idea of Proof of Work for DDOS protection is that a client who wants to get some resource from the server must first solve some problem issued by the server. To complete the task, the client's computing resources are required, which means that implementing a DDOS attack becomes more expensive for the attacker. At the same time, on the server side, checking the result of the task performed by the client requires practically no resources.
I looked at several existing algorithms:
Disadvantages of current algorithms:
- In a Merkle tree, the server has to do too much work to verify the client's solution. Checking the solution is not possible in O(1)
- In Hashcash, the client can make calculations in advance and use them for queries. You can combat this by storing tasks issued to clients on the server side.
Based on the shortcomings of existing algorithms, I chose to implement my own based on Hashcash. The algorithm is based on two things:
- Signing the issued task with a secret key. This makes it difficult for the client to precompute tasks.
- Instead of explicitly looking for leading zeros in the hash, a bitwise shift and big.Int comparison are used to check the solution.
However, despite the signature, the client can continue to reuse the solution within another connection. Therefore, you should not indicate too much time for the client to solve the problem. In a production environment, this drawback can be eliminated by using a centralized cache.
The server optionally accepts in-memory cache. If the cache is nil, the server will not be able to check that it has already checked the Challange due to the request. This potentially makes it possible to send the same result of the completed Challenge several times during its lifespan.
Structure used for PoW:
type Challenge struct {
Signature []byte `json:"sig"`
UnixTimestamp int64 `json:"unix"`
Nonce int64 `json:"nonce"`
Rand []byte `json:"rand"`
Difficulty uint8 `json:"dif"`
}
Task verification algorithm complexity O(1), and works as follows:
- Select the difficulty of the task in the range from 0 to 255, for example 20
- Shift one byte to the left, 1 << 255-20. The lower the complexity is selected, the smaller the shift will be and the larger the resulting value will be.
- At the output we get a large number represented by big.Int
- Calculate sha256 for Rand and Nonce
- Big.Int is created from the resulting hash amount
- The resulting big.Int must be less than the number from point 3
Existing directories:
- client - client package for external usage
- cmd/server - main.go for server
- cmd/client - main.go for client
- internal/config - config files for server and client
- internal/handler - example handler
- internal/pow - logic of PoW
- internal/proto - protocol implementation
- internal/server - tcp server implementation
- Dynamic difficulty change.
- Integrations tests.
- Implement the ability to support handlers with request parameters.
- Configuring client and server using options pattern
- Interface for the cache of issued Challenges