go-dos-protector, v1.1.0
Posted on

A library implementing denial-of-service attack (DoS attack) protection using the Proof-of-Work (PoW) algorithm.
Added an adapter layer to integrate PoW-based DoS attack protection with HTTP servers and clients.
Change Log
The format is based on Keep a Changelog.
Added
- Middlewares:
LoadLevelMiddleware: tracks the current server load by counting in-flight requests:- Intended to be used in conjunction with the dynamic hash difficulty provider.
- Interacts with the latter via an interface.
ResourceMiddleware: sets the request URL as the protected resource in the request context:- Optionally enriches the URL with a host:
- Host can be taken from the request itself.
- Host can be taken from proxy-provided headers.
- Optionally enriches the URL with a host:
DoSProtectorMiddleware: implements the core logic of DoS attack protection using the PoW algorithm:- If a request lacks the solution header
X-Dos-Protector-Solution, it generates a new challenge, signs it, and returns it via the response headersX-Dos-Protector-ChallengeandX-Dos-Protector-Signature. - If a request includes the solution header
X-Dos-Protector-Solution, it parses and validates the solution:- If validation fails, the
403 Forbiddenerror response is returned. - If validation succeeds, the request proceeds to the protected handler.
- If validation fails, the
- All operations are delegated to the corresponding use case via an interface.
- If a request lacks the solution header
- Models:
- Introduced adapter-layer models:
Challenge: corresponds to the domain-level challenge entity.Solution: corresponds to the domain-level solution entity.
- Functions:
NewChallengeFromEntity()andNewSolutionFromEntity(): convert domain entities into adapter-layer models.ParseChallengeFromQuery()andParseSolutionFromQuery(): parse adapter-layer models from URL-encoded query strings.
- Methods:
Challenge.ToQuery()andSolution.ToQuery(): serialize adapter-layer models into URL-encoded query strings.
- Introduced adapter-layer models:
- Errors:
TransformErrorToStatusCode()maps internal errors to appropriate HTTP status codes:- Internal error
dosProtectorUsecaseErrors.ErrInvalidParameterscorresponds to the HTTP status code400 Bad Request. - Internal error
powErrors.ErrValidationFailurecorresponds to the HTTP status code403 Forbidden. - Other errors correspond to the HTTP status code
500 Internal Server Error.
- Internal error
- Clients:
HTTPClientWrapper: a wrapper around the standard HTTP client (via an interface) that automates interaction withDoSProtectorMiddleware(see above):- Sends an initial
HEADrequest to the target URL to retrieve the challenge and signature from theX-Dos-Protector-ChallengeandX-Dos-Protector-Signatureheaders, respectively. - Parses and solves the challenge by invoking the corresponding use case via an interface.
- Clones the original request and enriches it with the computed solution and signature in the headers
X-Dos-Protector-SolutionandX-Dos-Protector-Signature. - Sends the enriched request to the server as usual.
- Sends an initial
- Tests:
newTestHTTPClient()andnewTestServer()for reusable test setup.- Integration tests for middleware and client interaction with constant and dynamic providers.
- Docs:
newExampleHTTPClient()andnewExampleServer()for reusable example setup.- Demonstrations of usage with constant and dynamic providers.
Changed
- Refactored
usecases/models:- Renamed
MessageAuthenticationCodefields toSignaturefor clarity.
- Renamed
Features
- use of patterns:
- implementation based on Clean Architecture principles:
- separate use case layers for server and client;
- adapter layer for integrating with HTTP servers and clients;
- input parsing and validation handled internally in the use cases (inputs are passed as raw DTOs);
- relies on the library
github.com/thewizardplusplus/go-powfor PoW algorithm implementation;
- implementation based on Clean Architecture principles:
- use cases:
- server-side:
SignChallenge(): generate a message authentication code (MAC) signature for a challenge:- MAC signature generation uses a secret key and configurable hashing algorithm;
GenerateChallenge(): generate a challenge with specified parameters:- number of leading zero bits (hash difficulty);
- current timestamp rounded to a configurable precision;
- time to live (TTL);
- target resource URI;
- payload consisting of static and random parts;
- hashing algorithm;
GenerateSignedChallenge(): generate a challenge and sign it;VerifySolution(): verify the correctness of a PoW solution;VerifySolutionAndChallengeSignature(): verify both PoW solution and challenge MAC signature;
- client-side:
SolveChallenge(): solve a challenge using the PoW algorithm;
- providers:
- extensible provider interfaces for:
- hash difficulty;
- target resource URI;
- static payload part;
- built-in provider implementations:
- extensible provider interfaces for:
- server-side:
- adapter layer:
- middlewares:
LoadLevelMiddleware: tracks the current server load by counting in-flight requests:- intended to be used in conjunction with the dynamic hash difficulty provider (see above);
- interacts with the latter via an interface;
ResourceMiddleware: sets the request URL as the protected resource in the request context:- optionally enriches the URL with a host:
- host can be taken from the request itself;
- host can be taken from proxy-provided headers;
- optionally enriches the URL with a host:
DoSProtectorMiddleware: implements the core logic of DoS attack protection using the PoW algorithm:- if a request lacks the solution header
X-Dos-Protector-Solution, it generates a new challenge, signs it, and returns it via the response headersX-Dos-Protector-ChallengeandX-Dos-Protector-Signature; - if a request includes the solution header
X-Dos-Protector-Solution, it parses and validates the solution:- if validation fails, the
403 Forbiddenerror response is returned; - if validation succeeds, the request proceeds to the protected handler;
- if validation fails, the
- all operations are delegated to the corresponding use case via an interface;
- if a request lacks the solution header
- models:
- introduced adapter-layer models:
Challenge: corresponds to the domain-level challenge entity;Solution: corresponds to the domain-level solution entity;
- functions:
NewChallengeFromEntity()andNewSolutionFromEntity(): convert domain entities into adapter-layer models;ParseChallengeFromQuery()andParseSolutionFromQuery(): parse adapter-layer models from URL-encoded query strings;
- methods:
Challenge.ToQuery()andSolution.ToQuery(): serialize adapter-layer models into URL-encoded query strings;
- introduced adapter-layer models:
- errors:
TransformErrorToStatusCode()maps internal errors to appropriate HTTP status codes:- internal error
dosProtectorUsecaseErrors.ErrInvalidParameterscorresponds to the HTTP status code400 Bad Request; - internal error
powErrors.ErrValidationFailurecorresponds to the HTTP status code403 Forbidden; - other errors correspond to the HTTP status code
500 Internal Server Error;
- internal error
- clients:
HTTPClientWrapper: a wrapper around the standard HTTP client (via an interface) that automates interaction withDoSProtectorMiddleware(see above):- sends an initial
HEADrequest to the target URL to retrieve the challenge and signature from theX-Dos-Protector-ChallengeandX-Dos-Protector-Signatureheaders, respectively; - parses and solves the challenge by invoking the corresponding use case via an interface;
- clones the original request and enriches it with the computed solution and signature in the headers
X-Dos-Protector-SolutionandX-Dos-Protector-Signature; - sends the enriched request to the server as usual.
- sends an initial
- middlewares:
Examples
With the constant providers:
package main
import (
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"fmt"
"io"
"log"
"math/big"
"net/http"
"net/http/httptest"
"time"
"github.com/samber/mo"
dosProtectorAdapterClients "github.com/thewizardplusplus/go-dos-protector/adapters/clients"
dosProtectorAdapterMiddlewares "github.com/thewizardplusplus/go-dos-protector/adapters/middlewares"
dosProtectorUsecases "github.com/thewizardplusplus/go-dos-protector/usecases"
dosProtectorUsecaseProviders "github.com/thewizardplusplus/go-dos-protector/usecases/providers"
powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)
func newExampleHashProvider() dosProtectorUsecases.HashProvider {
hashProvider := dosProtectorUsecaseProviders.NewMapHashProvider()
hashProvider.RegisterHash("SHA-256", sha256.New)
hashProvider.RegisterHash("SHA-512", sha512.New)
return hashProvider
}
func newExampleHTTPClient(
hashProvider dosProtectorUsecases.HashProvider,
) dosProtectorAdapterClients.HTTPClientWrapper {
return dosProtectorAdapterClients.NewHTTPClientWrapper(
dosProtectorAdapterClients.HTTPClientWrapperOptions{
HTTPClient: http.DefaultClient,
DoSProtectorUsecase: dosProtectorUsecases.NewClientDoSProtectorUsecase(
dosProtectorUsecases.ClientDoSProtectorUsecaseOptions{
HashProvider: hashProvider,
},
),
MaxAttemptCount: mo.Some(1000),
RandomInitialNonceParams: mo.Some(powValueTypes.RandomNonceParams{
RandomReader: rand.Reader,
MinRawValue: big.NewInt(1023),
MaxRawValue: big.NewInt(1042),
}),
},
)
}
type middleware func(handler http.Handler) http.Handler
type newExampleServerParams struct {
leadingZeroBitCountProvider dosProtectorUsecases.LeadingZeroBitCountProvider
resourceProvider dosProtectorUsecases.ResourceProvider
mainSerializedPayloadProvider dosProtectorUsecases.SerializedPayloadProvider
hashProvider dosProtectorUsecases.HashProvider
handler http.Handler
middlewares []middleware
}
func newExampleServer(params newExampleServerParams) (*httptest.Server, error) {
ttl, err := powValueTypes.NewTTL(10 * time.Minute)
if err != nil {
return nil, fmt.Errorf("unable to construct the TTL: %w", err)
}
dosProtectorMiddleware :=
dosProtectorAdapterMiddlewares.NewDoSProtectorMiddleware(
dosProtectorAdapterMiddlewares.DoSProtectorMiddlewareOptions{
DoSProtectorUsecase: dosProtectorUsecases.NewServerDoSProtectorUsecase(
dosProtectorUsecases.ServerDoSProtectorUsecaseOptions{
LeadingZeroBitCountProvider: params.leadingZeroBitCountProvider,
CreatedAtModulus: ttl.ToDuration(),
TTL: ttl,
ResourceProvider: params.resourceProvider,
MainSerializedPayloadProvider: params.mainSerializedPayloadProvider,
RandomPayloadByteReader: rand.Reader,
RandomPayloadByteCount: 128,
HashProvider: params.hashProvider,
GenerationHashName: "SHA-256",
SecretKey: "secret-key",
SigningHashName: "SHA-512",
},
),
HTTPErrorHandler: http.Error,
},
)
middlewares := make([]middleware, 0, len(params.middlewares)+1)
middlewares = append(middlewares, dosProtectorMiddleware.ApplyTo)
middlewares = append(middlewares, params.middlewares...)
handler := params.handler
for _, middleware := range middlewares {
handler = middleware(handler)
}
return httptest.NewServer(handler), nil
}
func main() {
hashProvider := newExampleHashProvider()
leadingZeroBitCount, err := powValueTypes.NewLeadingZeroBitCount(5)
if err != nil {
log.Fatalf("unable to construct the leading zero bit count: %s", err)
}
resource, err := powValueTypes.ParseResource("https://example.com/")
if err != nil {
log.Fatalf("unable to construct the resource: %s", err)
}
var mux http.ServeMux
mux.Handle("GET /api/v1/echo", http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
writer.Write([]byte("Hello, World!\n")) //nolint:errcheck
}))
leadingZeroBitCountProvider :=
dosProtectorUsecaseProviders.NewConstantLeadingZeroBitCount(
leadingZeroBitCount,
)
resourceProvider := dosProtectorUsecaseProviders.NewConstantResource(resource)
mainSerializedPayloadProvider :=
dosProtectorUsecaseProviders.NewConstantSerializedPayload(
powValueTypes.NewSerializedPayload("dummy"),
)
server, err := newExampleServer(newExampleServerParams{
leadingZeroBitCountProvider: leadingZeroBitCountProvider,
resourceProvider: resourceProvider,
mainSerializedPayloadProvider: mainSerializedPayloadProvider,
hashProvider: hashProvider,
handler: &mux,
middlewares: []middleware{},
})
if err != nil {
log.Fatalf("unable to construct the example server: %s", err)
}
defer server.Close()
request, err := http.NewRequest(http.MethodGet, server.URL+"/api/v1/echo", nil)
if err != nil {
log.Fatalf("unable to construct the request: %s", err)
}
httpClient := newExampleHTTPClient(hashProvider)
response, err := httpClient.Do(request)
if err != nil {
log.Fatalf("unable to send the request: %s", err)
}
defer response.Body.Close()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("unable to read the response body: %s", err)
}
fmt.Println(response.Status)
fmt.Print(string(responseBody))
// Output:
// 200 OK
// Hello, World!
}
With the dynamic providers:
package main
import (
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"fmt"
"io"
"log"
"math/big"
"net/http"
"net/http/httptest"
"time"
"github.com/samber/mo"
dosProtectorAdapterClients "github.com/thewizardplusplus/go-dos-protector/adapters/clients"
dosProtectorAdapterMiddlewares "github.com/thewizardplusplus/go-dos-protector/adapters/middlewares"
dosProtectorUsecases "github.com/thewizardplusplus/go-dos-protector/usecases"
dosProtectorUsecaseProviders "github.com/thewizardplusplus/go-dos-protector/usecases/providers"
powValueTypes "github.com/thewizardplusplus/go-pow/value-types"
)
func newExampleHashProvider() dosProtectorUsecases.HashProvider {
hashProvider := dosProtectorUsecaseProviders.NewMapHashProvider()
hashProvider.RegisterHash("SHA-256", sha256.New)
hashProvider.RegisterHash("SHA-512", sha512.New)
return hashProvider
}
func newExampleHTTPClient(
hashProvider dosProtectorUsecases.HashProvider,
) dosProtectorAdapterClients.HTTPClientWrapper {
return dosProtectorAdapterClients.NewHTTPClientWrapper(
dosProtectorAdapterClients.HTTPClientWrapperOptions{
HTTPClient: http.DefaultClient,
DoSProtectorUsecase: dosProtectorUsecases.NewClientDoSProtectorUsecase(
dosProtectorUsecases.ClientDoSProtectorUsecaseOptions{
HashProvider: hashProvider,
},
),
MaxAttemptCount: mo.Some(1000),
RandomInitialNonceParams: mo.Some(powValueTypes.RandomNonceParams{
RandomReader: rand.Reader,
MinRawValue: big.NewInt(1023),
MaxRawValue: big.NewInt(1042),
}),
},
)
}
type middleware func(handler http.Handler) http.Handler
type newExampleServerParams struct {
leadingZeroBitCountProvider dosProtectorUsecases.LeadingZeroBitCountProvider
resourceProvider dosProtectorUsecases.ResourceProvider
mainSerializedPayloadProvider dosProtectorUsecases.SerializedPayloadProvider
hashProvider dosProtectorUsecases.HashProvider
handler http.Handler
middlewares []middleware
}
func newExampleServer(params newExampleServerParams) (*httptest.Server, error) {
ttl, err := powValueTypes.NewTTL(10 * time.Minute)
if err != nil {
return nil, fmt.Errorf("unable to construct the TTL: %w", err)
}
dosProtectorMiddleware :=
dosProtectorAdapterMiddlewares.NewDoSProtectorMiddleware(
dosProtectorAdapterMiddlewares.DoSProtectorMiddlewareOptions{
DoSProtectorUsecase: dosProtectorUsecases.NewServerDoSProtectorUsecase(
dosProtectorUsecases.ServerDoSProtectorUsecaseOptions{
LeadingZeroBitCountProvider: params.leadingZeroBitCountProvider,
CreatedAtModulus: ttl.ToDuration(),
TTL: ttl,
ResourceProvider: params.resourceProvider,
MainSerializedPayloadProvider: params.mainSerializedPayloadProvider,
RandomPayloadByteReader: rand.Reader,
RandomPayloadByteCount: 128,
HashProvider: params.hashProvider,
GenerationHashName: "SHA-256",
SecretKey: "secret-key",
SigningHashName: "SHA-512",
},
),
HTTPErrorHandler: http.Error,
},
)
middlewares := make([]middleware, 0, len(params.middlewares)+1)
middlewares = append(middlewares, dosProtectorMiddleware.ApplyTo)
middlewares = append(middlewares, params.middlewares...)
handler := params.handler
for _, middleware := range middlewares {
handler = middleware(handler)
}
return httptest.NewServer(handler), nil
}
func main() {
hashProvider := newExampleHashProvider()
leadingZeroBitCountProvider, err :=
dosProtectorUsecaseProviders.NewDynamicLeadingZeroBitCount(
dosProtectorUsecaseProviders.DynamicLeadingZeroBitCountOptions{
MinConsideredLoadLevel: 1e3,
MaxConsideredLoadLevel: 1e4,
MinRawValue: 5,
MaxRawValue: 10,
},
)
if err != nil {
log.Fatalf("unable to construct the leading zero bit count provider: %s", err)
}
var mux http.ServeMux
mux.Handle("GET /api/v1/echo", http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
writer.Write([]byte("Hello, World!\n")) //nolint:errcheck
}))
serializedPayloadMiddleware := func(handler http.Handler) http.Handler {
return http.HandlerFunc(func(
writer http.ResponseWriter,
request *http.Request,
) {
handler.ServeHTTP(
writer,
request.WithContext(dosProtectorUsecaseProviders.WithSerializedPayload(
request.Context(),
powValueTypes.NewSerializedPayload(request.Header.Get("User-Agent")),
)),
)
})
}
resourceMiddleware := dosProtectorAdapterMiddlewares.NewResourceMiddleware(
dosProtectorAdapterMiddlewares.ResourceMiddlewareOptions{
HostMode: dosProtectorAdapterMiddlewares.AddHostFromRequest,
},
)
loadLevelMiddleware := dosProtectorAdapterMiddlewares.NewLoadLevelMiddleware(
dosProtectorAdapterMiddlewares.LoadLevelMiddlewareOptions{
LoadLevelRegister: leadingZeroBitCountProvider,
},
)
var resourceProvider dosProtectorUsecaseProviders.DynamicResource
var mainSerializedPayloadProvider dosProtectorUsecaseProviders.DynamicSerializedPayload //nolint:lll
server, err := newExampleServer(newExampleServerParams{
leadingZeroBitCountProvider: leadingZeroBitCountProvider,
resourceProvider: resourceProvider,
mainSerializedPayloadProvider: mainSerializedPayloadProvider,
hashProvider: hashProvider,
handler: &mux,
middlewares: []middleware{
serializedPayloadMiddleware,
resourceMiddleware.ApplyTo,
loadLevelMiddleware.ApplyTo,
},
})
if err != nil {
log.Fatalf("unable to construct the example server: %s", err)
}
defer server.Close()
request, err := http.NewRequest(http.MethodGet, server.URL+"/api/v1/echo", nil)
if err != nil {
log.Fatalf("unable to construct the request: %s", err)
}
httpClient := newExampleHTTPClient(hashProvider)
response, err := httpClient.Do(request)
if err != nil {
log.Fatalf("unable to send the request: %s", err)
}
defer response.Body.Close()
responseBody, err := io.ReadAll(response.Body)
if err != nil {
log.Fatalf("unable to read the response body: %s", err)
}
fmt.Println(response.Status)
fmt.Print(string(responseBody))
// Output:
// 200 OK
// Hello, World!
}
Repository
Link: https://github.com/thewizardplusplus/go-dos-protector/tree/v1.1.0.
Content: code.
License: MIT.