From 9369237229fa42e8424fc96d9a73bac160edc3c2 Mon Sep 17 00:00:00 2001 From: Alan Yeung Date: Sun, 20 Aug 2023 22:29:15 -0700 Subject: [PATCH 01/11] updated EAB --- .gitignore | 4 +++- src/acme.go | 2 +- src/mod/acme/acme.go | 41 +++++++++++++++++++++++++++++++++------ src/mod/acme/autorenew.go | 2 +- 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index e164ceb..42aede7 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,6 @@ src/rules/* src/README.md docker/ContainerTester.sh docker/ImagePublisher.sh -src/mod/acme/test/stackoverflow.pem \ No newline at end of file +src/mod/acme/test/stackoverflow.pem +src/sys.uuid +src/sys.db.lock diff --git a/src/acme.go b/src/acme.go index 72e5883..1bacb51 100644 --- a/src/acme.go +++ b/src/acme.go @@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler { port = getRandomPort(30000) } - return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port)) + return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port), "", "") } // create the special routing rule for ACME diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go index ec8bc1c..12f8581 100644 --- a/src/mod/acme/acme.go +++ b/src/mod/acme/acme.go @@ -54,18 +54,22 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey { type ACMEHandler struct { DefaultAcmeServer string Port string + Kid string + HmacEncoded string } // NewACME creates a new ACMEHandler instance. -func NewACME(acmeServer string, port string) *ACMEHandler { +func NewACME(acmeServer string, port string, kid string, hmacEncoded string) *ACMEHandler { return &ACMEHandler{ DefaultAcmeServer: acmeServer, Port: port, + Kid: kid, + HmacEncoded: hmacEncoded, } } // ObtainCert obtains a certificate for the specified domains. -func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string) (bool, error) { +func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, ca string, kid string, hmacEncoded string) (bool, error) { log.Println("[ACME] Obtaining certificate...") // generate private key @@ -113,12 +117,37 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email return false, err } + var reg *registration.Resource // New users will need to register - reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - if err != nil { - log.Println(err) - return false, err + if client.GetExternalAccountRequired() { + log.Println("External Account Required for this ACME Provider.") + // IF KID and HmacEncoded is overidden + if kid != "" && hmacEncoded != "" { + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: kid, + HmacEncoded: hmacEncoded, + }) + } else { + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: a.Kid, + HmacEncoded: a.HmacEncoded, + }) + } + if err != nil { + log.Println(err) + return false, err + } + //return false, errors.New("External Account Required for this ACME Provider.") + } else { + reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + if err != nil { + log.Println(err) + return false, err + } } + adminUser.Registration = reg // obtain the certificate diff --git a/src/mod/acme/autorenew.go b/src/mod/acme/autorenew.go index 211e168..28bee75 100644 --- a/src/mod/acme/autorenew.go +++ b/src/mod/acme/autorenew.go @@ -355,7 +355,7 @@ func (a *AutoRenewer) renewExpiredDomains(certs []*ExpiredCerts) ([]string, erro log.Println("Renewing " + expiredCert.Filepath + " (Might take a few minutes)") fileName := filepath.Base(expiredCert.Filepath) certName := fileName[:len(fileName)-len(filepath.Ext(fileName))] - _, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA) + _, err := a.AcmeHandler.ObtainCert(expiredCert.Domains, certName, a.RenewerConfig.Email, expiredCert.CA, "", "") if err != nil { log.Println("Renew " + fileName + "(" + strings.Join(expiredCert.Domains, ",") + ") failed: " + err.Error()) } else { From b1a14872c3fe7ce41e15606d4a2012422131b57e Mon Sep 17 00:00:00 2001 From: Alan Yeung Date: Sat, 2 Sep 2023 18:56:04 -0700 Subject: [PATCH 02/11] EAB implementation done --- src/acme.go | 2 +- src/mod/acme/acme.go | 72 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 17 deletions(-) diff --git a/src/acme.go b/src/acme.go index 1bacb51..3b13a8c 100644 --- a/src/acme.go +++ b/src/acme.go @@ -38,7 +38,7 @@ func initACME() *acme.ACMEHandler { port = getRandomPort(30000) } - return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port), "", "") + return acme.NewACME("https://acme-staging-v02.api.letsencrypt.org/directory", strconv.Itoa(port), "./conf/acme_auth.json") } // create the special routing rule for ACME diff --git a/src/mod/acme/acme.go b/src/mod/acme/acme.go index b25c4ac..049f941 100644 --- a/src/mod/acme/acme.go +++ b/src/mod/acme/acme.go @@ -9,6 +9,7 @@ import ( "crypto/x509" "encoding/json" "encoding/pem" + "errors" "fmt" "io/ioutil" "log" @@ -41,6 +42,11 @@ type ACMEUser struct { key crypto.PrivateKey } +type EABConfig struct { + Kid string `json:"kid"` + HmacKey string `json:"HmacKey"` +} + // GetEmail returns the email of the ACMEUser. func (u *ACMEUser) GetEmail() string { return u.Email @@ -58,24 +64,35 @@ func (u *ACMEUser) GetPrivateKey() crypto.PrivateKey { // ACMEHandler handles ACME-related operations. type ACMEHandler struct { - DefaultAcmeServer string - Port string - Kid string - HmacEncoded string + DefaultAcmeServer string + Port string + AuthConfigLocation string } // NewACME creates a new ACMEHandler instance. -func NewACME(acmeServer string, port string, kid string, hmacEncoded string) *ACMEHandler { +func NewACME(acmeServer string, port string, authConfigLocation string) *ACMEHandler { + + if !utils.FileExists(authConfigLocation) { + //Create one + os.MkdirAll(filepath.Dir(authConfigLocation), 0775) + js := []byte("{ \"EXAMPLE_CONFIG_PUT_YOUR_SERVICE_DIRECTORY_URL_HERE\": { \"kid\": \"PUT_YOUR_KID_HERE\", \"HmacKey\": \"PUT_YOUR_HMAC_HERE\" } }") + err := os.WriteFile(authConfigLocation, js, 0775) + if err != nil { + log.Fatal("[ACME] Failed to write ACME EAB config") + return nil + } + } + return &ACMEHandler{ - DefaultAcmeServer: acmeServer, - Port: port, - Kid: kid, - HmacEncoded: hmacEncoded, + DefaultAcmeServer: acmeServer, + Port: port, + AuthConfigLocation: authConfigLocation, } } // ObtainCert obtains a certificate for the specified domains. -func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool, kid string, hmacEncoded string) (bool, error) { +// , kid string, hmacEncoded string +func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email string, caName string, caUrl string, skipTLS bool) (bool, error) { log.Println("[ACME] Obtaining certificate...") // generate private key @@ -152,18 +169,18 @@ func (a *ACMEHandler) ObtainCert(domains []string, certificateName string, email if client.GetExternalAccountRequired() { log.Println("External Account Required for this ACME Provider.") // IF KID and HmacEncoded is overidden + kid, hmacEncoded, err := a.getKidHmac(config.CADirURL) + if err != nil { + log.Println(err) + return false, err + } + log.Println("EAB Credential retrieved.") if kid != "" && hmacEncoded != "" { reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: true, Kid: kid, HmacEncoded: hmacEncoded, }) - } else { - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: a.Kid, - HmacEncoded: a.HmacEncoded, - }) } if err != nil { log.Println(err) @@ -400,3 +417,26 @@ func loadCertInfoJSON(filename string) (*CertificateInfoJSON, error) { return certInfo, nil } + +func (a *ACMEHandler) getKidHmac(caName string) (string, string, error) { + var config map[string]EABConfig + + jsonData, err := ioutil.ReadFile(a.AuthConfigLocation) + if err != nil { + return "", "", err + } + + err = json.Unmarshal([]byte(jsonData), &config) + if err != nil { + return "", "", err + } + + // Access the values for dynamic keys + for key, value := range config { + if key == caName { + return value.Kid, value.HmacKey, nil + } + } + + return "", "", errors.New("Unable to find appropiate EAB information") +} From e3f8c99ed3c7ad69574e1176b6219edf6c5de03a Mon Sep 17 00:00:00 2001 From: kirari04 Date: Mon, 10 Jun 2024 17:52:16 +0200 Subject: [PATCH 03/11] poc of an ratelimit implementation --- src/mod/dynamicproxy/Server.go | 7 ++ src/mod/dynamicproxy/dynamicproxy.go | 7 ++ src/mod/dynamicproxy/ratelimit.go | 133 +++++++++++++++++++++++++++ src/reverseproxy.go | 4 + 4 files changed, 151 insertions(+) create mode 100644 src/mod/dynamicproxy/ratelimit.go diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 2771059..8756964 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -72,6 +72,13 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } + // Rate Limit Check + // if sep.RequireBasicAuth { + if err := handleRateLimit(w, r, sep); err != nil { + return + } + // } + //Validate basic auth if sep.RequireBasicAuth { err := h.handleBasicAuthRouting(w, r, sep) diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 7532d09..1395734 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -129,6 +129,13 @@ func (router *Router) StartProxyService() error { } } + // Rate Limit Check + // if sep.RequireBasicAuth { + if err := handleRateLimit(w, r, sep); err != nil { + return + } + // } + //Validate basic auth if sep.RequireBasicAuth { err := handleBasicAuth(w, r, sep) diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go new file mode 100644 index 0000000..e9884d7 --- /dev/null +++ b/src/mod/dynamicproxy/ratelimit.go @@ -0,0 +1,133 @@ +package dynamicproxy + +import ( + "errors" + "net" + "net/http" + "sync" + "time" + + "log" +) + +/* + ratelimit.go + + This file handles the ratelimit on proxy endpoints + if RateLimit is set to true +*/ + +// idk what this was for +// func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { +// err := handleRateLimit(w, r, pe) +// if err != nil { +// h.logRequest(r, false, 429, "host", pe.Domain) +// } +// return err +// } + +type IpTable struct { + sync.RWMutex + table map[string]*IpTableValue +} + +// Get the ip from the table +func (t *IpTable) Get(ip string) (*IpTableValue, bool) { + t.RLock() + defer t.RUnlock() + v, ok := t.table[ip] + return v, ok +} + +// Clear the ip from the table +func (t *IpTable) Clear() { + t.Lock() + defer t.Unlock() + t.table = make(map[string]*IpTableValue) +} + +// Increment the count of requests for a given ip +// init ip in ipTable if not exists +func (t *IpTable) Increment(ip string) { + t.Lock() + defer t.Unlock() + v, ok := t.table[ip] + if !ok { + v = &IpTableValue{Count: 0, LastHit: time.Now()} + } + v.Count++ + t.table[ip] = v +} + +// Check if the ip is in the table and if it is, check if the count is less than the limit +func (t *IpTable) Exceeded(ip string, limit int64) bool { + t.RLock() + defer t.RUnlock() + v, ok := t.table[ip] + if !ok { + return false + } + if v.Count < limit { + return false + } + return true +} + +// Get the count of requests for a given ip +// returns 0 if ip is not in the table +func (t *IpTable) GetCount(ip string) int64 { + t.RLock() + defer t.RUnlock() + v, ok := t.table[ip] + if !ok { + return 0 + } + return v.Count +} + +type IpTableValue struct { + Count int64 + LastHit time.Time +} + +var ipTable IpTable = IpTable{table: make(map[string]*IpTableValue)} + +// Handle rate limit logic +// do not write to http.ResponseWriter if err return is not nil (already handled by this function) +func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + // if len(pe.BasicAuthExceptionRules) > 0 { + // //Check if the current path matches the exception rules + // for _, exceptionRule := range pe.BasicAuthExceptionRules { + // if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) { + // //This path is excluded from basic auth + // return nil + // } + // } + // } + + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + w.WriteHeader(500) + log.Println("Error resolving remote address", r.RemoteAddr, err) + return errors.New("internal server error") + } + + ipTable.Increment(ip) + + // if ipTable.Exceeded(ip, pe.RateLimit) { + if ipTable.Exceeded(ip, 10) { + w.WriteHeader(429) + return errors.New("rate limit exceeded") + } + + log.Println("Rate limit check", ip, ipTable.GetCount(ip)) + + return nil +} + +func InitRateLimit() { + for { + ipTable.Clear() + time.Sleep(time.Second) + } +} diff --git a/src/reverseproxy.go b/src/reverseproxy.go index e2113da..81ffa35 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -146,6 +146,10 @@ func ReverseProxtInit() { SystemWideLogger.Println("Uptime Monitor background service started") }() + // Init Rate Limit + go func() { + dynamicproxy.InitRateLimit() + }() } func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) { From 6026c4fd5333b79981f7f1bb2904b40f9738dff9 Mon Sep 17 00:00:00 2001 From: kirari04 Date: Tue, 11 Jun 2024 16:40:04 +0200 Subject: [PATCH 04/11] implement sync.Map and atomic values with benchmark --- src/mod/dynamicproxy/ratelimit.go | 2 +- .../dynamicproxy/ratelimit_benchmark_test.go | 92 +++++++++++++++++++ src/mod/dynamicproxy/ratelimitb.go | 79 ++++++++++++++++ src/mod/dynamicproxy/ratelimitc.go | 78 ++++++++++++++++ src/mod/dynamicproxy/typedef.go | 4 + 5 files changed, 254 insertions(+), 1 deletion(-) create mode 100644 src/mod/dynamicproxy/ratelimit_benchmark_test.go create mode 100644 src/mod/dynamicproxy/ratelimitb.go create mode 100644 src/mod/dynamicproxy/ratelimitc.go diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go index e9884d7..e173fb7 100644 --- a/src/mod/dynamicproxy/ratelimit.go +++ b/src/mod/dynamicproxy/ratelimit.go @@ -120,7 +120,7 @@ func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) return errors.New("rate limit exceeded") } - log.Println("Rate limit check", ip, ipTable.GetCount(ip)) + // log.Println("Rate limit check", ip, ipTable.GetCount(ip)) return nil } diff --git a/src/mod/dynamicproxy/ratelimit_benchmark_test.go b/src/mod/dynamicproxy/ratelimit_benchmark_test.go new file mode 100644 index 0000000..ed5b846 --- /dev/null +++ b/src/mod/dynamicproxy/ratelimit_benchmark_test.go @@ -0,0 +1,92 @@ +package dynamicproxy + +import ( + "net/http" + "net/http/httptest" + "sync" + "testing" +) + +func BenchmarkRateLimitSyncMapInt64(b *testing.B) { + ipTableSyncMapInt64 = IpTableSyncMapInt64{} // Reset the table + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRateLimitSyncMapInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) + }) + + request := httptest.NewRequest("GET", "/", nil) + request.RemoteAddr = "192.168.1.1:1234" + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + } +} + +func BenchmarkRateLimitSyncMapAtomicInt64(b *testing.B) { + ipTableSyncMapAtomicInt64 = IpTableSyncMapAtomicInt64{} // Reset the table + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRateLimitSyncMapAtomicInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) + }) + + request := httptest.NewRequest("GET", "/", nil) + request.RemoteAddr = "192.168.1.1:1234" + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + } +} + +func BenchmarkRateLimitSyncMapInt64Concurrent(b *testing.B) { + ipTableSyncMapInt64 = IpTableSyncMapInt64{} // Reset the table + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRateLimitSyncMapInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) + }) + + request := httptest.NewRequest("GET", "/", nil) + request.RemoteAddr = "192.168.1.1:1234" + + b.ResetTimer() + + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + }() + } + wg.Wait() +} + +func BenchmarkRateLimitSyncMapAtomicInt64Concurrent(b *testing.B) { + ipTableSyncMapAtomicInt64 = IpTableSyncMapAtomicInt64{} // Reset the table + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRateLimitSyncMapAtomicInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) + }) + + request := httptest.NewRequest("GET", "/", nil) + request.RemoteAddr = "192.168.1.1:1234" + + b.ResetTimer() + + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + }() + } + wg.Wait() +} diff --git a/src/mod/dynamicproxy/ratelimitb.go b/src/mod/dynamicproxy/ratelimitb.go new file mode 100644 index 0000000..48c749b --- /dev/null +++ b/src/mod/dynamicproxy/ratelimitb.go @@ -0,0 +1,79 @@ +package dynamicproxy + +import ( + "errors" + "log" + "net" + "net/http" + "sync" + "time" +) + +// IpTableSyncMapInt64 is a rate limiter implementation using sync.Map with int64 +type IpTableSyncMapInt64 struct { + table sync.Map +} + +// Increment the count of requests for a given IP +func (t *IpTableSyncMapInt64) Increment(ip string) { + v, _ := t.table.LoadOrStore(ip, new(int64)) + count := v.(*int64) + *count++ +} + +// Check if the IP is in the table and if it is, check if the count is less than the limit +func (t *IpTableSyncMapInt64) Exceeded(ip string, limit int64) bool { + v, ok := t.table.Load(ip) + if !ok { + return false + } + count := v.(*int64) + return *count >= limit +} + +// Get the count of requests for a given IP +func (t *IpTableSyncMapInt64) GetCount(ip string) int64 { + v, ok := t.table.Load(ip) + if !ok { + return 0 + } + count := v.(*int64) + return *count +} + +// Clear the IP table +func (t *IpTableSyncMapInt64) Clear() { + t.table.Range(func(key, value interface{}) bool { + t.table.Delete(key) + return true + }) +} + +var ipTableSyncMapInt64 = IpTableSyncMapInt64{} + +func handleRateLimitSyncMapInt64(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + w.WriteHeader(500) + log.Println("Error resolving remote address", r.RemoteAddr, err) + return errors.New("internal server error") + } + + ipTableSyncMapInt64.Increment(ip) + + if ipTableSyncMapInt64.Exceeded(ip, 10) { + w.WriteHeader(429) + return errors.New("rate limit exceeded") + } + + // log.Println("Rate limit check", ip, ipTableSyncMapInt64.GetCount(ip)) + + return nil +} + +func InitRateLimitSyncMapInt64() { + for { + ipTableSyncMapInt64.Clear() + time.Sleep(time.Second) + } +} diff --git a/src/mod/dynamicproxy/ratelimitc.go b/src/mod/dynamicproxy/ratelimitc.go new file mode 100644 index 0000000..88d67ea --- /dev/null +++ b/src/mod/dynamicproxy/ratelimitc.go @@ -0,0 +1,78 @@ +package dynamicproxy + +import ( + "errors" + "log" + "net" + "net/http" + "sync" + "sync/atomic" + "time" +) + +// IpTableSyncMapAtomicInt64 is a rate limiter implementation using sync.Map with atomic int64 +type IpTableSyncMapAtomicInt64 struct { + table sync.Map +} + +// Increment the count of requests for a given IP +func (t *IpTableSyncMapAtomicInt64) Increment(ip string) { + v, _ := t.table.LoadOrStore(ip, new(int64)) + atomic.AddInt64(v.(*int64), 1) +} + +// Check if the IP is in the table and if it is, check if the count is less than the limit +func (t *IpTableSyncMapAtomicInt64) Exceeded(ip string, limit int64) bool { + v, ok := t.table.Load(ip) + if !ok { + return false + } + count := atomic.LoadInt64(v.(*int64)) + return count >= limit +} + +// Get the count of requests for a given IP +func (t *IpTableSyncMapAtomicInt64) GetCount(ip string) int64 { + v, ok := t.table.Load(ip) + if !ok { + return 0 + } + return atomic.LoadInt64(v.(*int64)) +} + +// Clear the IP table +func (t *IpTableSyncMapAtomicInt64) Clear() { + t.table.Range(func(key, value interface{}) bool { + t.table.Delete(key) + return true + }) +} + +var ipTableSyncMapAtomicInt64 = IpTableSyncMapAtomicInt64{} + +func handleRateLimitSyncMapAtomicInt64(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + w.WriteHeader(500) + log.Println("Error resolving remote address", r.RemoteAddr, err) + return errors.New("internal server error") + } + + ipTableSyncMapAtomicInt64.Increment(ip) + + if ipTableSyncMapAtomicInt64.Exceeded(ip, 10) { + w.WriteHeader(429) + return errors.New("rate limit exceeded") + } + + // log.Println("Rate limit check", ip, ipTableSyncMapAtomicInt64.GetCount(ip)) + + return nil +} + +func InitRateLimitSyncMapAtomicInt64() { + for { + ipTableSyncMapAtomicInt64.Clear() + time.Sleep(time.Second) + } +} diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 3386aa6..660e663 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -124,6 +124,10 @@ type ProxyEndpoint struct { BasicAuthCredentials []*BasicAuthCredentials //Basic auth credentials BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target + // Rate Limiting + EnableRateLimiting bool + RateLimiting int // Rate limit in requests per second + //Access Control AccessFilterUUID string //Access filter ID From 61e4d45430685fecd51f8e85c9cfefc0cab359f3 Mon Sep 17 00:00:00 2001 From: kirari04 Date: Tue, 11 Jun 2024 16:53:29 +0200 Subject: [PATCH 05/11] improoved benchmark --- .../dynamicproxy/ratelimit_benchmark_test.go | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/src/mod/dynamicproxy/ratelimit_benchmark_test.go b/src/mod/dynamicproxy/ratelimit_benchmark_test.go index ed5b846..f19a40b 100644 --- a/src/mod/dynamicproxy/ratelimit_benchmark_test.go +++ b/src/mod/dynamicproxy/ratelimit_benchmark_test.go @@ -7,9 +7,25 @@ import ( "testing" ) -func BenchmarkRateLimitSyncMapInt64(b *testing.B) { - ipTableSyncMapInt64 = IpTableSyncMapInt64{} // Reset the table +func BenchmarkRateLimitLockMapInt64(b *testing.B) { + go InitRateLimit() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRateLimit(w, r, &ProxyEndpoint{RateLimiting: 10}) + }) + request := httptest.NewRequest("GET", "/", nil) + request.RemoteAddr = "192.168.1.1:1234" + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + } +} + +func BenchmarkRateLimitSyncMapInt64(b *testing.B) { + go InitRateLimitSyncMapInt64() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handleRateLimitSyncMapInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) }) @@ -26,8 +42,7 @@ func BenchmarkRateLimitSyncMapInt64(b *testing.B) { } func BenchmarkRateLimitSyncMapAtomicInt64(b *testing.B) { - ipTableSyncMapAtomicInt64 = IpTableSyncMapAtomicInt64{} // Reset the table - + go InitRateLimitSyncMapAtomicInt64() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handleRateLimitSyncMapAtomicInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) }) @@ -43,9 +58,31 @@ func BenchmarkRateLimitSyncMapAtomicInt64(b *testing.B) { } } -func BenchmarkRateLimitSyncMapInt64Concurrent(b *testing.B) { - ipTableSyncMapInt64 = IpTableSyncMapInt64{} // Reset the table +func BenchmarkRateLimitLockMapInt64Concurrent(b *testing.B) { + go InitRateLimit() + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + handleRateLimit(w, r, &ProxyEndpoint{RateLimiting: 10}) + }) + request := httptest.NewRequest("GET", "/", nil) + request.RemoteAddr = "192.168.1.1:1234" + + b.ResetTimer() + + var wg sync.WaitGroup + for i := 0; i < b.N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + recorder := httptest.NewRecorder() + handler.ServeHTTP(recorder, request) + }() + } + wg.Wait() +} + +func BenchmarkRateLimitSyncMapInt64Concurrent(b *testing.B) { + go InitRateLimitSyncMapInt64() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handleRateLimitSyncMapInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) }) @@ -68,8 +105,7 @@ func BenchmarkRateLimitSyncMapInt64Concurrent(b *testing.B) { } func BenchmarkRateLimitSyncMapAtomicInt64Concurrent(b *testing.B) { - ipTableSyncMapAtomicInt64 = IpTableSyncMapAtomicInt64{} // Reset the table - + go InitRateLimitSyncMapAtomicInt64() handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handleRateLimitSyncMapAtomicInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) }) From 95453431517ca3b4b52404a70d14016e9c4e266e Mon Sep 17 00:00:00 2001 From: kirari04 Date: Tue, 11 Jun 2024 16:56:59 +0200 Subject: [PATCH 06/11] Removing Benchmark & Updated implementation --- src/mod/dynamicproxy/ratelimit.go | 97 +++---------- .../dynamicproxy/ratelimit_benchmark_test.go | 128 ------------------ src/mod/dynamicproxy/ratelimitb.go | 79 ----------- src/mod/dynamicproxy/ratelimitc.go | 78 ----------- 4 files changed, 21 insertions(+), 361 deletions(-) delete mode 100644 src/mod/dynamicproxy/ratelimit_benchmark_test.go delete mode 100644 src/mod/dynamicproxy/ratelimitb.go delete mode 100644 src/mod/dynamicproxy/ratelimitc.go diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go index e173fb7..cdcb96c 100644 --- a/src/mod/dynamicproxy/ratelimit.go +++ b/src/mod/dynamicproxy/ratelimit.go @@ -2,109 +2,55 @@ package dynamicproxy import ( "errors" + "log" "net" "net/http" "sync" + "sync/atomic" "time" - - "log" ) -/* - ratelimit.go - - This file handles the ratelimit on proxy endpoints - if RateLimit is set to true -*/ - -// idk what this was for -// func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { -// err := handleRateLimit(w, r, pe) -// if err != nil { -// h.logRequest(r, false, 429, "host", pe.Domain) -// } -// return err -// } - +// IpTable is a rate limiter implementation using sync.Map with atomic int64 type IpTable struct { - sync.RWMutex - table map[string]*IpTableValue + table sync.Map } -// Get the ip from the table -func (t *IpTable) Get(ip string) (*IpTableValue, bool) { - t.RLock() - defer t.RUnlock() - v, ok := t.table[ip] - return v, ok -} - -// Clear the ip from the table -func (t *IpTable) Clear() { - t.Lock() - defer t.Unlock() - t.table = make(map[string]*IpTableValue) -} - -// Increment the count of requests for a given ip -// init ip in ipTable if not exists +// Increment the count of requests for a given IP func (t *IpTable) Increment(ip string) { - t.Lock() - defer t.Unlock() - v, ok := t.table[ip] - if !ok { - v = &IpTableValue{Count: 0, LastHit: time.Now()} - } - v.Count++ - t.table[ip] = v + v, _ := t.table.LoadOrStore(ip, new(int64)) + atomic.AddInt64(v.(*int64), 1) } -// Check if the ip is in the table and if it is, check if the count is less than the limit +// Check if the IP is in the table and if it is, check if the count is less than the limit func (t *IpTable) Exceeded(ip string, limit int64) bool { - t.RLock() - defer t.RUnlock() - v, ok := t.table[ip] + v, ok := t.table.Load(ip) if !ok { return false } - if v.Count < limit { - return false - } - return true + count := atomic.LoadInt64(v.(*int64)) + return count >= limit } -// Get the count of requests for a given ip -// returns 0 if ip is not in the table +// Get the count of requests for a given IP func (t *IpTable) GetCount(ip string) int64 { - t.RLock() - defer t.RUnlock() - v, ok := t.table[ip] + v, ok := t.table.Load(ip) if !ok { return 0 } - return v.Count + return atomic.LoadInt64(v.(*int64)) } -type IpTableValue struct { - Count int64 - LastHit time.Time +// Clear the IP table +func (t *IpTable) Clear() { + t.table.Range(func(key, value interface{}) bool { + t.table.Delete(key) + return true + }) } -var ipTable IpTable = IpTable{table: make(map[string]*IpTableValue)} +var ipTable = IpTable{} -// Handle rate limit logic -// do not write to http.ResponseWriter if err return is not nil (already handled by this function) func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { - // if len(pe.BasicAuthExceptionRules) > 0 { - // //Check if the current path matches the exception rules - // for _, exceptionRule := range pe.BasicAuthExceptionRules { - // if strings.HasPrefix(r.RequestURI, exceptionRule.PathPrefix) { - // //This path is excluded from basic auth - // return nil - // } - // } - // } - ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { w.WriteHeader(500) @@ -114,7 +60,6 @@ func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) ipTable.Increment(ip) - // if ipTable.Exceeded(ip, pe.RateLimit) { if ipTable.Exceeded(ip, 10) { w.WriteHeader(429) return errors.New("rate limit exceeded") diff --git a/src/mod/dynamicproxy/ratelimit_benchmark_test.go b/src/mod/dynamicproxy/ratelimit_benchmark_test.go deleted file mode 100644 index f19a40b..0000000 --- a/src/mod/dynamicproxy/ratelimit_benchmark_test.go +++ /dev/null @@ -1,128 +0,0 @@ -package dynamicproxy - -import ( - "net/http" - "net/http/httptest" - "sync" - "testing" -) - -func BenchmarkRateLimitLockMapInt64(b *testing.B) { - go InitRateLimit() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRateLimit(w, r, &ProxyEndpoint{RateLimiting: 10}) - }) - - request := httptest.NewRequest("GET", "/", nil) - request.RemoteAddr = "192.168.1.1:1234" - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - } -} - -func BenchmarkRateLimitSyncMapInt64(b *testing.B) { - go InitRateLimitSyncMapInt64() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRateLimitSyncMapInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) - }) - - request := httptest.NewRequest("GET", "/", nil) - request.RemoteAddr = "192.168.1.1:1234" - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - } -} - -func BenchmarkRateLimitSyncMapAtomicInt64(b *testing.B) { - go InitRateLimitSyncMapAtomicInt64() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRateLimitSyncMapAtomicInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) - }) - - request := httptest.NewRequest("GET", "/", nil) - request.RemoteAddr = "192.168.1.1:1234" - - b.ResetTimer() - - for i := 0; i < b.N; i++ { - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - } -} - -func BenchmarkRateLimitLockMapInt64Concurrent(b *testing.B) { - go InitRateLimit() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRateLimit(w, r, &ProxyEndpoint{RateLimiting: 10}) - }) - - request := httptest.NewRequest("GET", "/", nil) - request.RemoteAddr = "192.168.1.1:1234" - - b.ResetTimer() - - var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - go func() { - defer wg.Done() - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - }() - } - wg.Wait() -} - -func BenchmarkRateLimitSyncMapInt64Concurrent(b *testing.B) { - go InitRateLimitSyncMapInt64() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRateLimitSyncMapInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) - }) - - request := httptest.NewRequest("GET", "/", nil) - request.RemoteAddr = "192.168.1.1:1234" - - b.ResetTimer() - - var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - go func() { - defer wg.Done() - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - }() - } - wg.Wait() -} - -func BenchmarkRateLimitSyncMapAtomicInt64Concurrent(b *testing.B) { - go InitRateLimitSyncMapAtomicInt64() - handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - handleRateLimitSyncMapAtomicInt64(w, r, &ProxyEndpoint{RateLimiting: 10}) - }) - - request := httptest.NewRequest("GET", "/", nil) - request.RemoteAddr = "192.168.1.1:1234" - - b.ResetTimer() - - var wg sync.WaitGroup - for i := 0; i < b.N; i++ { - wg.Add(1) - go func() { - defer wg.Done() - recorder := httptest.NewRecorder() - handler.ServeHTTP(recorder, request) - }() - } - wg.Wait() -} diff --git a/src/mod/dynamicproxy/ratelimitb.go b/src/mod/dynamicproxy/ratelimitb.go deleted file mode 100644 index 48c749b..0000000 --- a/src/mod/dynamicproxy/ratelimitb.go +++ /dev/null @@ -1,79 +0,0 @@ -package dynamicproxy - -import ( - "errors" - "log" - "net" - "net/http" - "sync" - "time" -) - -// IpTableSyncMapInt64 is a rate limiter implementation using sync.Map with int64 -type IpTableSyncMapInt64 struct { - table sync.Map -} - -// Increment the count of requests for a given IP -func (t *IpTableSyncMapInt64) Increment(ip string) { - v, _ := t.table.LoadOrStore(ip, new(int64)) - count := v.(*int64) - *count++ -} - -// Check if the IP is in the table and if it is, check if the count is less than the limit -func (t *IpTableSyncMapInt64) Exceeded(ip string, limit int64) bool { - v, ok := t.table.Load(ip) - if !ok { - return false - } - count := v.(*int64) - return *count >= limit -} - -// Get the count of requests for a given IP -func (t *IpTableSyncMapInt64) GetCount(ip string) int64 { - v, ok := t.table.Load(ip) - if !ok { - return 0 - } - count := v.(*int64) - return *count -} - -// Clear the IP table -func (t *IpTableSyncMapInt64) Clear() { - t.table.Range(func(key, value interface{}) bool { - t.table.Delete(key) - return true - }) -} - -var ipTableSyncMapInt64 = IpTableSyncMapInt64{} - -func handleRateLimitSyncMapInt64(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - w.WriteHeader(500) - log.Println("Error resolving remote address", r.RemoteAddr, err) - return errors.New("internal server error") - } - - ipTableSyncMapInt64.Increment(ip) - - if ipTableSyncMapInt64.Exceeded(ip, 10) { - w.WriteHeader(429) - return errors.New("rate limit exceeded") - } - - // log.Println("Rate limit check", ip, ipTableSyncMapInt64.GetCount(ip)) - - return nil -} - -func InitRateLimitSyncMapInt64() { - for { - ipTableSyncMapInt64.Clear() - time.Sleep(time.Second) - } -} diff --git a/src/mod/dynamicproxy/ratelimitc.go b/src/mod/dynamicproxy/ratelimitc.go deleted file mode 100644 index 88d67ea..0000000 --- a/src/mod/dynamicproxy/ratelimitc.go +++ /dev/null @@ -1,78 +0,0 @@ -package dynamicproxy - -import ( - "errors" - "log" - "net" - "net/http" - "sync" - "sync/atomic" - "time" -) - -// IpTableSyncMapAtomicInt64 is a rate limiter implementation using sync.Map with atomic int64 -type IpTableSyncMapAtomicInt64 struct { - table sync.Map -} - -// Increment the count of requests for a given IP -func (t *IpTableSyncMapAtomicInt64) Increment(ip string) { - v, _ := t.table.LoadOrStore(ip, new(int64)) - atomic.AddInt64(v.(*int64), 1) -} - -// Check if the IP is in the table and if it is, check if the count is less than the limit -func (t *IpTableSyncMapAtomicInt64) Exceeded(ip string, limit int64) bool { - v, ok := t.table.Load(ip) - if !ok { - return false - } - count := atomic.LoadInt64(v.(*int64)) - return count >= limit -} - -// Get the count of requests for a given IP -func (t *IpTableSyncMapAtomicInt64) GetCount(ip string) int64 { - v, ok := t.table.Load(ip) - if !ok { - return 0 - } - return atomic.LoadInt64(v.(*int64)) -} - -// Clear the IP table -func (t *IpTableSyncMapAtomicInt64) Clear() { - t.table.Range(func(key, value interface{}) bool { - t.table.Delete(key) - return true - }) -} - -var ipTableSyncMapAtomicInt64 = IpTableSyncMapAtomicInt64{} - -func handleRateLimitSyncMapAtomicInt64(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - w.WriteHeader(500) - log.Println("Error resolving remote address", r.RemoteAddr, err) - return errors.New("internal server error") - } - - ipTableSyncMapAtomicInt64.Increment(ip) - - if ipTableSyncMapAtomicInt64.Exceeded(ip, 10) { - w.WriteHeader(429) - return errors.New("rate limit exceeded") - } - - // log.Println("Rate limit check", ip, ipTableSyncMapAtomicInt64.GetCount(ip)) - - return nil -} - -func InitRateLimitSyncMapAtomicInt64() { - for { - ipTableSyncMapAtomicInt64.Clear() - time.Sleep(time.Second) - } -} From bb1b161ae2feac235ecef9e63da3f662f5cc9f44 Mon Sep 17 00:00:00 2001 From: kirari04 Date: Tue, 11 Jun 2024 22:04:30 +0200 Subject: [PATCH 07/11] clean up implementation --- src/mod/dynamicproxy/Server.go | 9 +++++---- src/mod/dynamicproxy/ratelimit.go | 10 +++++++++- src/mod/dynamicproxy/typedef.go | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index 8756964..f89fa26 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -73,11 +73,12 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Rate Limit Check - // if sep.RequireBasicAuth { - if err := handleRateLimit(w, r, sep); err != nil { - return + if sep.RequireRateLimit { + err := h.handleRateLimitRouting(w, r, sep) + if err != nil { + return + } } - // } //Validate basic auth if sep.RequireBasicAuth { diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go index cdcb96c..0ac4e9f 100644 --- a/src/mod/dynamicproxy/ratelimit.go +++ b/src/mod/dynamicproxy/ratelimit.go @@ -50,6 +50,14 @@ func (t *IpTable) Clear() { var ipTable = IpTable{} +func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + err := handleRateLimit(w, r, pe) + if err != nil { + h.logRequest(r, false, 429, "ratelimit", pe.Domain) + } + return err +} + func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { @@ -60,7 +68,7 @@ func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) ipTable.Increment(ip) - if ipTable.Exceeded(ip, 10) { + if ipTable.Exceeded(ip, int64(pe.RateLimit)) { w.WriteHeader(429) return errors.New("rate limit exceeded") } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index 660e663..d4f9a8a 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -125,8 +125,8 @@ type ProxyEndpoint struct { BasicAuthExceptionRules []*BasicAuthExceptionRule //Path to exclude in a basic auth enabled proxy target // Rate Limiting - EnableRateLimiting bool - RateLimiting int // Rate limit in requests per second + RequireRateLimit bool + RateLimit int64 // Rate limit in requests per second //Access Control AccessFilterUUID string //Access filter ID From fa11422748065c95924bd2a44b6b3bb908c06c47 Mon Sep 17 00:00:00 2001 From: kirari04 Date: Tue, 11 Jun 2024 22:36:03 +0200 Subject: [PATCH 08/11] Implemented ui part for rate limit --- .gitignore | 6 ++++++ src/reverseproxy.go | 23 +++++++++++++++++++++++ src/web/components/httprp.html | 4 ++++ src/web/components/rules.html | 25 +++++++++++++++++++++++++ 4 files changed, 58 insertions(+) diff --git a/.gitignore b/.gitignore index 26006a7..532fdbe 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,9 @@ docker/ImagePublisher.sh src/mod/acme/test/stackoverflow.pem /tools/dns_challenge_update/code-gen/acmedns /tools/dns_challenge_update/code-gen/lego +src/tmp/localhost.key +src/tmp/localhost.pem +src/www/html/index.html +src/sys.uuid +src/zoraxy +src/log/zr_2024-6.log diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 81ffa35..fa980a9 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -233,6 +233,26 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { requireBasicAuth := (rba == "true") + // Require Rate Limiting? + rl, _ := utils.PostPara(r, "rate") + if rl == "" { + rl = "false" + } + requireRateLimit := (rl == "true") + rlnum, _ := utils.PostPara(r, "ratenum") + if rlnum == "" { + rlnum = "0" + } + proxyRateLimit, err := strconv.ParseInt(rlnum, 10, 64) + if err != nil { + utils.SendErrorResponse(w, "invalid rate limit number") + return + } + if proxyRateLimit <= 0 { + utils.SendErrorResponse(w, "rate limit number must be greater than 0") + return + } + // Bypass WebSocket Origin Check strbpwsorg, _ := utils.PostPara(r, "bpwsorg") if strbpwsorg == "" { @@ -313,6 +333,9 @@ func ReverseProxyHandleAddEndpoint(w http.ResponseWriter, r *http.Request) { BasicAuthExceptionRules: []*dynamicproxy.BasicAuthExceptionRule{}, DefaultSiteOption: 0, DefaultSiteValue: "", + // Rate Limit + RequireRateLimit: requireRateLimit, + RateLimit: proxyRateLimit, } preparedEndpoint, err := dynamicProxyRouter.PrepareProxyRoute(&thisProxyEndpoint) diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index b96dedc..e9e4f49 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -20,6 +20,7 @@ Destination Virtual Directory Basic Auth + Rate Limit Actions @@ -107,6 +108,9 @@ ${subd.RequireBasicAuth?``:``} + + ${subd.RequireRateLimit?` ${subd.RateLimit}req/s`:``} +
diff --git a/src/web/components/rules.html b/src/web/components/rules.html index 6a4faad..9a8474e 100644 --- a/src/web/components/rules.html +++ b/src/web/components/rules.html @@ -73,6 +73,17 @@
+
+
+ + +
+
+
+ + + The Rate Limit is applied to the whole proxy endpoint. If the number of requests exceeds the limit, the proxy will return a 429 error code. +
@@ -147,6 +158,8 @@ var skipTLSValidation = $("#skipTLSValidation")[0].checked; var bypassGlobalTLS = $("#bypassGlobalTLS")[0].checked; var requireBasicAuth = $("#requireBasicAuth")[0].checked; + var proxyRateLimit = $("#proxyRateLimit").val(); + var requireRateLimit = $("#requireRateLimit")[0].checked; var skipWebSocketOriginCheck = $("#skipWebsocketOriginCheck")[0].checked; var accessRuleToUse = $("#newProxyRuleAccessFilter").val(); @@ -176,6 +189,8 @@ bpwsorg: skipWebSocketOriginCheck, bypassGlobalTLS: bypassGlobalTLS, bauth: requireBasicAuth, + rate: requireRateLimit, + ratenum: proxyRateLimit, cred: JSON.stringify(credentials), access: accessRuleToUse, }, @@ -264,6 +279,16 @@ } $("#requireBasicAuth").on('change', toggleBasicAuth); toggleBasicAuth(); + + function toggleRateLimit() { + if ($("#requireRateLimit").parent().checkbox("is checked")) { + $("#proxyRateLimit").parent().removeClass("disabled"); + } else { + $("#proxyRateLimit").parent().addClass("disabled"); + } + } + $("#requireRateLimit").on('change', toggleRateLimit); + toggleRateLimit(); /* From 49babbd60f4315459de42594fb1700576b5d4877 Mon Sep 17 00:00:00 2001 From: kirari04 Date: Tue, 11 Jun 2024 22:45:46 +0200 Subject: [PATCH 09/11] implemented update ratelimit --- src/reverseproxy.go | 22 ++++++++++++++++++++++ src/web/components/httprp.html | 21 +++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/reverseproxy.go b/src/reverseproxy.go index fa980a9..9e7664f 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -457,6 +457,26 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { requireBasicAuth := (rba == "true") + // Rate Limiting? + rl, _ := utils.PostPara(r, "rate") + if rl == "" { + rl = "false" + } + requireRateLimit := (rl == "true") + rlnum, _ := utils.PostPara(r, "ratenum") + if rlnum == "" { + rlnum = "0" + } + proxyRateLimit, err := strconv.ParseInt(rlnum, 10, 64) + if err != nil { + utils.SendErrorResponse(w, "invalid rate limit number") + return + } + if proxyRateLimit <= 0 { + utils.SendErrorResponse(w, "rate limit number must be greater than 0") + return + } + // Bypass WebSocket Origin Check strbpwsorg, _ := utils.PostPara(r, "bpwsorg") if strbpwsorg == "" { @@ -478,6 +498,8 @@ func ReverseProxyHandleEditEndpoint(w http.ResponseWriter, r *http.Request) { newProxyEndpoint.BypassGlobalTLS = bypassGlobalTLS newProxyEndpoint.SkipCertValidations = skipTlsValidation newProxyEndpoint.RequireBasicAuth = requireBasicAuth + newProxyEndpoint.RequireRateLimit = requireRateLimit + newProxyEndpoint.RateLimit = proxyRateLimit newProxyEndpoint.SkipWebSocketOriginCheck = bypassWebsocketOriginCheck //Prepare to replace the current routing rule diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index e9e4f49..c660eb3 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -305,6 +305,23 @@
`); + } else if (datatype == "ratelimit"){ + let requireRateLimit = payload.RequireRateLimit; + let checkstate = ""; + if (requireRateLimit){ + checkstate = "checked"; + } + let rateLimit = payload.RateLimit; + + column.empty().append(`
+ + +
+
+ +
+ `); + }else if (datatype == 'action'){ column.empty().append(` @@ -352,6 +369,8 @@ let requireTLS = $(row).find(".RequireTLS")[0].checked; let skipCertValidations = $(row).find(".SkipCertValidations")[0].checked; let requireBasicAuth = $(row).find(".RequireBasicAuth")[0].checked; + let requireRateLimit = $(row).find(".RequireRateLimit")[0].checked; + let rateLimit = $(row).find(".RateLimit").val(); let bypassGlobalTLS = $(row).find(".BypassGlobalTLS")[0].checked; let bypassWebsocketOrigin = $(row).find(".SkipWebSocketOriginCheck")[0].checked; console.log(newDomain, requireTLS, skipCertValidations, requireBasicAuth) @@ -368,6 +387,8 @@ "tlsval": skipCertValidations, "bpwsorg" : bypassWebsocketOrigin, "bauth" :requireBasicAuth, + "rate" :requireRateLimit, + "ratenum" :rateLimit, }, success: function(data){ if (data.error !== undefined){ From 25c7e8ac1a68e2d889438a8ca8eedd35050a8046 Mon Sep 17 00:00:00 2001 From: kirari04 Date: Wed, 12 Jun 2024 18:00:08 +0200 Subject: [PATCH 10/11] update git ignore --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 532fdbe..6e386d5 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,4 @@ src/tmp/localhost.pem src/www/html/index.html src/sys.uuid src/zoraxy -src/log/zr_2024-6.log +src/log/ \ No newline at end of file From 10048150bbe732f913cdc15aed42f76a40e6ae88 Mon Sep 17 00:00:00 2001 From: Toby Chui Date: Fri, 14 Jun 2024 23:42:52 +0800 Subject: [PATCH 11/11] Optimized rate limiter implementation - Moved rate limiter scope into proxy router - Give IpTable a better name following clean code guideline - Optimized client IP retrieval method - Added stop channel for request counter ticker - Fixed #199 - Optimized UI for rate limit --- src/main.go | 4 +- src/mod/dynamicproxy/Server.go | 2 +- src/mod/dynamicproxy/dpcore/header.go | 1 - src/mod/dynamicproxy/dynamicproxy.go | 41 +++++++++---- src/mod/dynamicproxy/ratelimit.go | 75 +++++++++++++++++------- src/mod/dynamicproxy/typedef.go | 5 +- src/redirect.go | 2 +- src/reverseproxy.go | 5 -- src/web/components/httprp.html | 83 ++++++++++++++++++--------- src/web/components/rules.html | 21 ++++--- src/web/components/status.html | 2 +- 11 files changed, 160 insertions(+), 81 deletions(-) diff --git a/src/main.go b/src/main.go index d813665..44cf82b 100644 --- a/src/main.go +++ b/src/main.go @@ -52,9 +52,9 @@ var logOutputToFile = flag.Bool("log", true, "Log terminal output to file") var ( name = "Zoraxy" - version = "3.0.6" + version = "3.0.7" nodeUUID = "generic" - development = false //Set this to false to use embedded web fs + development = true //Set this to false to use embedded web fs bootTime = time.Now().Unix() /* diff --git a/src/mod/dynamicproxy/Server.go b/src/mod/dynamicproxy/Server.go index f89fa26..62a0e89 100644 --- a/src/mod/dynamicproxy/Server.go +++ b/src/mod/dynamicproxy/Server.go @@ -72,7 +72,7 @@ func (h *ProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // Rate Limit Check + // Rate Limit if sep.RequireRateLimit { err := h.handleRateLimitRouting(w, r, sep) if err != nil { diff --git a/src/mod/dynamicproxy/dpcore/header.go b/src/mod/dynamicproxy/dpcore/header.go index 40c029e..4c38ada 100644 --- a/src/mod/dynamicproxy/dpcore/header.go +++ b/src/mod/dynamicproxy/dpcore/header.go @@ -91,7 +91,6 @@ func addXForwardedForHeader(req *http.Request) { req.Header.Set("X-Real-Ip", strings.TrimSpace(ips[0])) } } - } } diff --git a/src/mod/dynamicproxy/dynamicproxy.go b/src/mod/dynamicproxy/dynamicproxy.go index 1395734..b0e9b5e 100644 --- a/src/mod/dynamicproxy/dynamicproxy.go +++ b/src/mod/dynamicproxy/dynamicproxy.go @@ -23,12 +23,12 @@ import ( func NewDynamicProxy(option RouterOption) (*Router, error) { proxyMap := sync.Map{} thisRouter := Router{ - Option: &option, - ProxyEndpoints: &proxyMap, - Running: false, - server: nil, - routingRules: []*RoutingRule{}, - tldMap: map[string]int{}, + Option: &option, + ProxyEndpoints: &proxyMap, + Running: false, + server: nil, + routingRules: []*RoutingRule{}, + rateLimitCounter: RequestCountPerIpTable{}, } thisRouter.mux = &ProxyHandler{ @@ -85,6 +85,12 @@ func (router *Router) StartProxyService() error { MinVersion: uint16(minVersion), } + //Start rate limitor + err := router.startRateLimterCounterResetTicker() + if err != nil { + return err + } + if router.Option.UseTls { router.server = &http.Server{ Addr: ":" + strconv.Itoa(router.Option.Port), @@ -129,12 +135,12 @@ func (router *Router) StartProxyService() error { } } - // Rate Limit Check - // if sep.RequireBasicAuth { - if err := handleRateLimit(w, r, sep); err != nil { - return + // Rate Limit + if sep.RequireRateLimit { + if err := router.handleRateLimit(w, r, sep); err != nil { + return + } } - // } //Validate basic auth if sep.RequireBasicAuth { @@ -239,10 +245,23 @@ func (router *Router) StopProxyService() error { return err } + //Stop TLS listener if router.tlsListener != nil { router.tlsListener.Close() } + //Stop rate limiter + if router.rateLimterStop != nil { + go func() { + // As the rate timer loop has a 1 sec ticker + // stop the rate limiter in go routine can prevent + // front end from freezing for 1 sec + router.rateLimterStop <- true + }() + + } + + //Stop TLS redirection (from port 80) if router.tlsRedirectStop != nil { router.tlsRedirectStop <- true } diff --git a/src/mod/dynamicproxy/ratelimit.go b/src/mod/dynamicproxy/ratelimit.go index 0ac4e9f..17969e7 100644 --- a/src/mod/dynamicproxy/ratelimit.go +++ b/src/mod/dynamicproxy/ratelimit.go @@ -2,27 +2,27 @@ package dynamicproxy import ( "errors" - "log" "net" "net/http" + "strings" "sync" "sync/atomic" "time" ) // IpTable is a rate limiter implementation using sync.Map with atomic int64 -type IpTable struct { +type RequestCountPerIpTable struct { table sync.Map } // Increment the count of requests for a given IP -func (t *IpTable) Increment(ip string) { +func (t *RequestCountPerIpTable) Increment(ip string) { v, _ := t.table.LoadOrStore(ip, new(int64)) atomic.AddInt64(v.(*int64), 1) } // Check if the IP is in the table and if it is, check if the count is less than the limit -func (t *IpTable) Exceeded(ip string, limit int64) bool { +func (t *RequestCountPerIpTable) Exceeded(ip string, limit int64) bool { v, ok := t.table.Load(ip) if !ok { return false @@ -32,7 +32,7 @@ func (t *IpTable) Exceeded(ip string, limit int64) bool { } // Get the count of requests for a given IP -func (t *IpTable) GetCount(ip string) int64 { +func (t *RequestCountPerIpTable) GetCount(ip string) int64 { v, ok := t.table.Load(ip) if !ok { return 0 @@ -41,34 +41,50 @@ func (t *IpTable) GetCount(ip string) int64 { } // Clear the IP table -func (t *IpTable) Clear() { +func (t *RequestCountPerIpTable) Clear() { t.table.Range(func(key, value interface{}) bool { t.table.Delete(key) return true }) } -var ipTable = IpTable{} - func (h *ProxyHandler) handleRateLimitRouting(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { - err := handleRateLimit(w, r, pe) + err := h.Parent.handleRateLimit(w, r, pe) if err != nil { h.logRequest(r, false, 429, "ratelimit", pe.Domain) } return err } -func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { - ip, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - w.WriteHeader(500) - log.Println("Error resolving remote address", r.RemoteAddr, err) - return errors.New("internal server error") +func (router *Router) handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) error { + //Get the real client-ip from request header + clientIP := r.RemoteAddr + if r.Header.Get("X-Real-Ip") == "" { + CF_Connecting_IP := r.Header.Get("CF-Connecting-IP") + Fastly_Client_IP := r.Header.Get("Fastly-Client-IP") + if CF_Connecting_IP != "" { + //Use CF Connecting IP + clientIP = CF_Connecting_IP + } else if Fastly_Client_IP != "" { + //Use Fastly Client IP + clientIP = Fastly_Client_IP + } else { + ips := strings.Split(clientIP, ",") + if len(ips) > 0 { + clientIP = strings.TrimSpace(ips[0]) + } + } } - ipTable.Increment(ip) + ip, _, err := net.SplitHostPort(clientIP) + if err != nil { + //Default allow passthrough on error + return nil + } - if ipTable.Exceeded(ip, int64(pe.RateLimit)) { + router.rateLimitCounter.Increment(ip) + + if router.rateLimitCounter.Exceeded(ip, int64(pe.RateLimit)) { w.WriteHeader(429) return errors.New("rate limit exceeded") } @@ -78,9 +94,26 @@ func handleRateLimit(w http.ResponseWriter, r *http.Request, pe *ProxyEndpoint) return nil } -func InitRateLimit() { - for { - ipTable.Clear() - time.Sleep(time.Second) +// Start the ticker routine for reseting the rate limit counter every seconds +func (r *Router) startRateLimterCounterResetTicker() error { + if r.rateLimterStop != nil { + return errors.New("another rate limiter ticker already running") } + tickerStopChan := make(chan bool) + r.rateLimterStop = tickerStopChan + + counterResetTicker := time.NewTicker(1 * time.Second) + go func() { + for { + select { + case <-tickerStopChan: + r.rateLimterStop = nil + return + case <-counterResetTicker.C: + r.rateLimitCounter.Clear() + } + } + }() + + return nil } diff --git a/src/mod/dynamicproxy/typedef.go b/src/mod/dynamicproxy/typedef.go index d4f9a8a..49c3253 100644 --- a/src/mod/dynamicproxy/typedef.go +++ b/src/mod/dynamicproxy/typedef.go @@ -51,8 +51,9 @@ type Router struct { tlsListener net.Listener routingRules []*RoutingRule - tlsRedirectStop chan bool //Stop channel for tls redirection server - tldMap map[string]int //Top level domain map, see tld.json + tlsRedirectStop chan bool //Stop channel for tls redirection server + rateLimterStop chan bool //Stop channel for rate limiter + rateLimitCounter RequestCountPerIpTable //Request counter for rate limter } // Auth credential for basic auth on certain endpoints diff --git a/src/redirect.go b/src/redirect.go index 824cbe9..fa0a5c2 100644 --- a/src/redirect.go +++ b/src/redirect.go @@ -91,7 +91,7 @@ func handleToggleRedirectRegexpSupport(w http.ResponseWriter, r *http.Request) { //Update the current regex support rule enable state enableRegexSupport := strings.EqualFold(strings.TrimSpace(enabled), "true") redirectTable.AllowRegex = enableRegexSupport - err = sysdb.Write("Redirect", "regex", enableRegexSupport) + err = sysdb.Write("redirect", "regex", enableRegexSupport) if enableRegexSupport { SystemWideLogger.PrintAndLog("redirect", "Regex redirect rule enabled", nil) diff --git a/src/reverseproxy.go b/src/reverseproxy.go index 9e7664f..b2c164f 100644 --- a/src/reverseproxy.go +++ b/src/reverseproxy.go @@ -145,11 +145,6 @@ func ReverseProxtInit() { }) SystemWideLogger.Println("Uptime Monitor background service started") }() - - // Init Rate Limit - go func() { - dynamicproxy.InitRateLimit() - }() } func ReverseProxyHandleOnOff(w http.ResponseWriter, r *http.Request) { diff --git a/src/web/components/httprp.html b/src/web/components/httprp.html index c660eb3..36bc297 100644 --- a/src/web/components/httprp.html +++ b/src/web/components/httprp.html @@ -19,8 +19,7 @@ Host Destination Virtual Directory - Basic Auth - Rate Limit + Advanced Settings Actions @@ -105,11 +104,9 @@ ${subd.Domain} ${tlsIcon} ${vdList} - - ${subd.RequireBasicAuth?``:``} - - - ${subd.RequireRateLimit?` ${subd.RateLimit}req/s`:``} + + ${subd.RequireBasicAuth?` Basic Auth`:` Basic Auth`}
+ ${subd.RequireRateLimit?` Rate Limit @ ${subd.RateLimit} req/s`:` Rate Limit`}
@@ -267,11 +264,11 @@ Edit Virtual Directories `); - }else if (datatype == "basicauth"){ + }else if (datatype == "advanced"){ let requireBasicAuth = payload.RequireBasicAuth; - let checkstate = ""; + let basicAuthCheckstate = ""; if (requireBasicAuth){ - checkstate = "checked"; + basicAuthCheckstate = "checked"; } let skipWebSocketOriginCheck = payload.SkipWebSocketOriginCheck; @@ -280,16 +277,36 @@ wsCheckstate = "checked"; } + let requireRateLimit = payload.RequireRateLimit; + let rateLimitCheckState = ""; + if (requireRateLimit){ + rateLimitCheckState = "checked"; + } + let rateLimit = payload.RateLimit; + if (rateLimit == 0){ + //This value is not set. Make it default to 100 + rateLimit = 100; + } + let rateLimitDisableState = ""; + if (!payload.RequireRateLimit){ + rateLimitDisableState = "disabled"; + } + column.empty().append(`
- +
- +
+ +
+ + +
- Advance Configs + Security Options
@@ -298,23 +315,26 @@ Check this to allow cross-origin websocket requests

- - +
+ + +

+
+ + +
`); } else if (datatype == "ratelimit"){ - let requireRateLimit = payload.RequireRateLimit; - let checkstate = ""; - if (requireRateLimit){ - checkstate = "checked"; - } - let rateLimit = payload.RateLimit; - - column.empty().append(`
- + + column.empty().append(` +
+
@@ -352,6 +372,17 @@ $("#httpProxyList").find(".editBtn").addClass("disabled"); } + //handleToggleRateLimitInput will get trigger if the "require rate limit" checkbox + // is changed and toggle the disable state of the rate limit input field + function handleToggleRateLimitInput(){ + let isRateLimitEnabled = $("#httpProxyList input.RequireRateLimit")[0].checked; + if (isRateLimitEnabled){ + $("#httpProxyList input.RateLimit").parent().removeClass("disabled"); + }else{ + $("#httpProxyList input.RateLimit").parent().addClass("disabled"); + } + } + function exitProxyInlineEdit(){ listProxyEndpoints(); $("#httpProxyList").find(".editBtn").removeClass("disabled"); @@ -463,10 +494,6 @@ }) } - /* Access List handling */ - - - //Bind on tab switch events tabSwitchEventBind["httprp"] = function(){ listProxyEndpoints(); diff --git a/src/web/components/rules.html b/src/web/components/rules.html index 9a8474e..e5b7913 100644 --- a/src/web/components/rules.html +++ b/src/web/components/rules.html @@ -81,8 +81,13 @@
- - The Rate Limit is applied to the whole proxy endpoint. If the number of requests exceeds the limit, the proxy will return a 429 error code. +
+ +
+ req / sec / IP +
+
+ Return a 429 error code if request rate exceed the rate limit.
@@ -282,9 +287,9 @@ function toggleRateLimit() { if ($("#requireRateLimit").parent().checkbox("is checked")) { - $("#proxyRateLimit").parent().removeClass("disabled"); + $("#proxyRateLimit").parent().parent().removeClass("disabled"); } else { - $("#proxyRateLimit").parent().addClass("disabled"); + $("#proxyRateLimit").parent().parent().addClass("disabled"); } } $("#requireRateLimit").on('change', toggleRateLimit); @@ -422,9 +427,9 @@ initNewProxyRuleAccessDropdownList(); } - $(document).ready(function(){ - $("#advanceProxyRules").accordion(); - $("#newProxyRuleAccessFilter").parent().dropdown(); - }); + + $("#advanceProxyRules").accordion(); + $("#newProxyRuleAccessFilter").parent().dropdown(); + \ No newline at end of file diff --git a/src/web/components/status.html b/src/web/components/status.html index 9bfd908..3d187bb 100644 --- a/src/web/components/status.html +++ b/src/web/components/status.html @@ -74,7 +74,7 @@
-

+