From 73a3c01280e98d6c77fc74ccfa707f059efb0bf4 Mon Sep 17 00:00:00 2001 From: Jeremy Yen Date: Tue, 10 Oct 2023 19:25:53 +0800 Subject: [PATCH 1/5] Implement challenge_session for kafka --- internal/config.go | 51 +++++++++++++++++++++++++++-------------- internal/http_server.go | 31 +++++++++++++++++++++++-- internal/kafka.go | 23 +++++++++++++++++-- 3 files changed, 84 insertions(+), 21 deletions(-) diff --git a/internal/config.go b/internal/config.go index 12f5678..51f3164 100644 --- a/internal/config.go +++ b/internal/config.go @@ -95,6 +95,7 @@ const ( type ExpiringDecision struct { Decision Decision Expires time.Time + IpAddress string fromBaskerville bool } @@ -133,6 +134,8 @@ type DecisionLists struct { PerSiteDecisionLists StringToStringToDecision // site -> ip -> Decision // dynamic lists populated from the regex rate limits + kafka ExpiringDecisionLists StringToExpiringDecision // ip -> ExpiringDecision + // dynamic lists populated from the kafka, like ExpiringDecisionLists but session ID as index + ExpiringDecisionListsSessionId StringToExpiringDecision // static site-wide lists (legacy banjax_sha_inv and user_banjax_sha_inv) // XXX someday need sha-inv *and* captcha // XXX could be merged with PerSiteDecisionLists if we matched on ip ranges @@ -220,6 +223,7 @@ func ConfigToDecisionLists(config *Config) DecisionLists { perSiteDecisionLists := make(StringToStringToDecision) globalDecisionLists := make(StringToDecision) expiringDecisionLists := make(StringToExpiringDecision) + expiringDecisionListsSessionId := make(StringToExpiringDecision) sitewideShaInvList := make(StringToFailAction) globalDecisionListsIPFilter := make(DecisionToIPFilter) perSiteDecisionListsIPFilter := make(StringToDecisionToIPFilter) @@ -292,7 +296,7 @@ func ConfigToDecisionLists(config *Config) DecisionLists { // log.Printf("global decisions: %v\n", globalDecisionLists) return DecisionLists{ globalDecisionLists, perSiteDecisionLists, - expiringDecisionLists, sitewideShaInvList, + expiringDecisionLists, expiringDecisionListsSessionId, sitewideShaInvList, globalDecisionListsIPFilter, perSiteDecisionListsIPFilter} } @@ -385,20 +389,6 @@ func (failedChallengeStates FailedChallengeStates) String() string { return buf.String() } -func checkExpiringDecisionLists(clientIp string, decisionLists *DecisionLists) (ExpiringDecision, bool) { - expiringDecision, ok := (*decisionLists).ExpiringDecisionLists[clientIp] - if !ok { - // log.Println("no mention in expiring lists") - } else { - if time.Now().Sub(expiringDecision.Expires) > 0 { - delete((*decisionLists).ExpiringDecisionLists, clientIp) - // log.Println("deleted expired decision from expiring lists") - ok = false - } - } - return expiringDecision, ok -} - // XXX mmm could hold the lock for a while? func RemoveExpiredDecisions( decisionListsMutex *sync.Mutex, @@ -430,7 +420,7 @@ func updateExpiringDecisionLists( existingExpiringDecision, ok := (*decisionLists).ExpiringDecisionLists[ip] if ok { if newDecision <= existingExpiringDecision.Decision { - log.Println("not updating expiringDecision with less serious one", existingExpiringDecision.Decision, newDecision) + // log.Println("not updating expiringDecision with less serious one", existingExpiringDecision.Decision, newDecision) return } } @@ -442,7 +432,34 @@ func updateExpiringDecisionLists( // XXX We are not using nginx to banjax cache feature yet // purgeNginxAuthCacheForIp(ip) expires := now.Add(time.Duration(config.ExpiringDecisionTtlSeconds) * time.Second) - (*decisionLists).ExpiringDecisionLists[ip] = ExpiringDecision{newDecision, expires, fromBaskerville} + (*decisionLists).ExpiringDecisionLists[ip] = ExpiringDecision{ + newDecision, expires, ip, fromBaskerville} +} + +func updateExpiringDecisionListsSessionId( + config *Config, + ip string, + sessionId string, + decisionListsMutex *sync.Mutex, + decisionLists *DecisionLists, + now time.Time, + newDecision Decision, + fromBaskerville bool, +) { + decisionListsMutex.Lock() + defer decisionListsMutex.Unlock() + + existingExpiringDecision, ok := (*decisionLists).ExpiringDecisionListsSessionId[sessionId] + if ok { + if newDecision <= existingExpiringDecision.Decision { + return + } + } + + // log.Printf("Update session id challenge with IP %s, session id %s, existing and new: %v, %v\n", ip, sessionId, existingExpiringDecision.Decision, newDecision) + expires := now.Add(time.Duration(config.ExpiringDecisionTtlSeconds) * time.Second) + (*decisionLists).ExpiringDecisionListsSessionId[sessionId] = ExpiringDecision{ + newDecision, expires, ip, fromBaskerville} } type MetricsLogLine struct { diff --git a/internal/http_server.go b/internal/http_server.go index b016845..b69d786 100644 --- a/internal/http_server.go +++ b/internal/http_server.go @@ -945,7 +945,7 @@ func decisionForNginx2( // changing the decision. // XXX i forget if that comment is stale^ decisionListsMutex.Lock() - expiringDecision, ok := checkExpiringDecisionLists(clientIp, decisionLists) + expiringDecision, ok := checkExpiringDecisionLists(c, clientIp, decisionLists) decisionListsMutex.Unlock() if !ok { // log.Println("no mention in expiring lists") @@ -960,7 +960,7 @@ func decisionForNginx2( // Check if expiringDecision.fromBaskerville, if true, check if domain disabled baskerville _, disabled := config.SitesToDisableBaskerville[requestedHost] if expiringDecision.fromBaskerville && disabled { - log.Printf("domain %s disabled baskerville, skip expiring challenge for %s", requestedHost, clientIp) + log.Printf("DIS-BASK: domain %s disabled baskerville, skip expiring challenge for %s", requestedHost, clientIp) } else { // log.Println("challenge from expiring lists") sendOrValidateShaChallengeResult := sendOrValidateShaChallenge( @@ -1032,3 +1032,30 @@ func CleanRequestedPath(requestedPath string) string { path = strings.Split(path, "?")[0] return path } + +func checkExpiringDecisionLists(c *gin.Context, clientIp string, decisionLists *DecisionLists) (ExpiringDecision, bool) { + // check session ID then check expiring lists IP + sessionId, err := c.Cookie(SessionCookieName) + if err == nil { + expiringDecision, ok := (*decisionLists).ExpiringDecisionListsSessionId[sessionId] + if ok { + if time.Now().Sub(expiringDecision.Expires) > 0 { + delete((*decisionLists).ExpiringDecisionListsSessionId, sessionId) + // log.Println("deleted expired decision from expiring lists") + ok = false + } + log.Printf("DSC: challenge expiring decision for %s from session %s", expiringDecision.IpAddress, sessionId) + return expiringDecision, ok + } + } + + expiringDecision, ok := (*decisionLists).ExpiringDecisionLists[clientIp] + if ok { + if time.Now().Sub(expiringDecision.Expires) > 0 { + delete((*decisionLists).ExpiringDecisionLists, clientIp) + // log.Println("deleted expired decision from expiring lists") + ok = false + } + } + return expiringDecision, ok +} diff --git a/internal/kafka.go b/internal/kafka.go index 40e43e9..9d6f783 100644 --- a/internal/kafka.go +++ b/internal/kafka.go @@ -157,12 +157,31 @@ func handleCommand( Challenge, true, // from baskerville, provide to http_server to distinguish from regex ) - log.Printf("KAFKA: added to global challenge lists: Challenge %s\n", command.Value) + log.Printf("KAFKA: challenge_ip: %s\n", command.Value) } else if disabled { - log.Printf("KAFKA: not challenge %s, site %s disables baskerville\n", command.Value, command.Host) + log.Printf("KAFKA: DIS-BASK: not challenge %s, site %s disabled baskerville\n", command.Value, command.Host) } else { log.Printf("KAFKA: command value looks malformed: %s\n", command.Value) } + case "challenge_session": + // exempt a site from challenge according to config + _, disabled := config.SitesToDisableBaskerville[command.Host] + + if !disabled { + updateExpiringDecisionListsSessionId( + config, + command.Value, + command.SessionId, + decisionListsMutex, + decisionLists, + time.Now(), + Challenge, + true, // from baskerville, provide to http_server to distinguish from regex + ) + log.Printf("KAFKA: challenge_session: %s\n", command.SessionId) + } else { + log.Printf("KAFKA: DIS-BASK: not challenge %s, site %s disabled baskerville\n", command.Value, command.Host) + } default: log.Printf("KAFKA: unrecognized command name: %s\n", command.Name) } From ee51f5f7130c2bac59cf466da6196288599e3f0f Mon Sep 17 00:00:00 2001 From: Jeremy Yen Date: Thu, 23 Nov 2023 02:24:39 +0800 Subject: [PATCH 2/5] Fixed challenge_session command --- banjax-config.yaml | 9 +++++---- internal/config.go | 7 +++++-- internal/http_server.go | 2 +- internal/kafka.go | 9 +++++++-- internal/session_cookie.go | 2 +- 5 files changed, 19 insertions(+), 10 deletions(-) diff --git a/banjax-config.yaml b/banjax-config.yaml index 76f8d69..706687e 100644 --- a/banjax-config.yaml +++ b/banjax-config.yaml @@ -85,11 +85,11 @@ regexes_with_rates: sitewide_sha_inv_list: example.com: block foobar.com: no_block - localhost: no_block + sub.localhost: no_block www.localhost: no_block server_log_file: /var/log/banjax/banjax-format.log banning_log_file: /etc/banjax/ban_ip_list.log -expiring_decision_ttl_seconds: 10 +expiring_decision_ttl_seconds: 100 too_many_failed_challenges_interval_seconds: 10 too_many_failed_challenges_threshold: 3 password_cookie_ttl_seconds: 345600 # Dynamic apply to internal/password-protected-path.html:170 @@ -108,6 +108,7 @@ banning_log_file_temp: /etc/banjax/ban_ip_list_temp.log session_cookie_hmac_secret: some_secret session_cookie_ttl_seconds: 3600 sites_to_disable_baskerville: - localhost: true + sub.localhost: false use_user_agent_in_cookie: - localhost: true + sub.localhost: true +session_cookie_not_verify: true diff --git a/internal/config.go b/internal/config.go index 51f3164..71fcf97 100644 --- a/internal/config.go +++ b/internal/config.go @@ -66,6 +66,7 @@ type Config struct { DisableKafka bool `yaml:"disable_kafka"` SessionCookieHmacSecret string `yaml:"session_cookie_hmac_secret"` SessionCookieTtlSeconds int `yaml:"session_cookie_ttl_seconds"` + SessionCookieNotVerify bool `yaml:"session_cookie_not_verify"` SitesToDisableBaskerville map[string]bool `yaml:"sites_to_disable_baskerville"` } @@ -426,7 +427,6 @@ func updateExpiringDecisionLists( } if config.Debug { log.Println("Update expiringDecision with existing and new: ", existingExpiringDecision.Decision, newDecision) - log.Println("From baskerville", fromBaskerville) } // XXX We are not using nginx to banjax cache feature yet @@ -456,7 +456,10 @@ func updateExpiringDecisionListsSessionId( } } - // log.Printf("Update session id challenge with IP %s, session id %s, existing and new: %v, %v\n", ip, sessionId, existingExpiringDecision.Decision, newDecision) + if config.Debug { + log.Printf("Update session id challenge with IP %s, session id %s, existing and new: %v, %v\n", + ip, sessionId, existingExpiringDecision.Decision, newDecision) + } expires := now.Add(time.Duration(config.ExpiringDecisionTtlSeconds) * time.Second) (*decisionLists).ExpiringDecisionListsSessionId[sessionId] = ExpiringDecision{ newDecision, expires, ip, fromBaskerville} diff --git a/internal/http_server.go b/internal/http_server.go index b69d786..13a43ad 100644 --- a/internal/http_server.go +++ b/internal/http_server.go @@ -1039,12 +1039,12 @@ func checkExpiringDecisionLists(c *gin.Context, clientIp string, decisionLists * if err == nil { expiringDecision, ok := (*decisionLists).ExpiringDecisionListsSessionId[sessionId] if ok { + log.Printf("DSC: found expiringDecision from session %s (%s)", sessionId, expiringDecision.Decision) if time.Now().Sub(expiringDecision.Expires) > 0 { delete((*decisionLists).ExpiringDecisionListsSessionId, sessionId) // log.Println("deleted expired decision from expiring lists") ok = false } - log.Printf("DSC: challenge expiring decision for %s from session %s", expiringDecision.IpAddress, sessionId) return expiringDecision, ok } } diff --git a/internal/kafka.go b/internal/kafka.go index 9d6f783..f44e009 100644 --- a/internal/kafka.go +++ b/internal/kafka.go @@ -12,6 +12,7 @@ import ( "crypto/x509" "encoding/json" "log" + "net/url" "os" "sync" "time" @@ -163,25 +164,29 @@ func handleCommand( } else { log.Printf("KAFKA: command value looks malformed: %s\n", command.Value) } + break case "challenge_session": // exempt a site from challenge according to config _, disabled := config.SitesToDisableBaskerville[command.Host] if !disabled { + // gin does urldecode or cookie, so we decode any possible urlencoded session id from kafka + sessionIdDecoded, _ := url.QueryUnescape(command.SessionId) updateExpiringDecisionListsSessionId( config, command.Value, - command.SessionId, + sessionIdDecoded, decisionListsMutex, decisionLists, time.Now(), Challenge, true, // from baskerville, provide to http_server to distinguish from regex ) - log.Printf("KAFKA: challenge_session: %s\n", command.SessionId) + log.Printf("KAFKA: challenge_session: %s\n", sessionIdDecoded) } else { log.Printf("KAFKA: DIS-BASK: not challenge %s, site %s disabled baskerville\n", command.Value, command.Host) } + break default: log.Printf("KAFKA: unrecognized command name: %s\n", command.Name) } diff --git a/internal/session_cookie.go b/internal/session_cookie.go index 0f68c03..4df2ea0 100644 --- a/internal/session_cookie.go +++ b/internal/session_cookie.go @@ -131,7 +131,7 @@ func sessionCookieEndPoint(c *gin.Context, config *Config) error { if err == nil { // cookie exists, validate it validateErr := validateSessionCookie(urlDecodedDsc, config.SessionCookieHmacSecret, time.Now(), clientIp) - if validateErr == nil { + if validateErr == nil || config.SessionCookieNotVerify { // cookie is valid, do not attach cookie but only report dsc_new=false // log.Printf("DSC: [%s] cookie %s is valid, report dsc_new=false\n", clientIp, urlDecodedDsc) attachSessionCookie(c, config, urlDecodedDsc, false) From 8207e38d20d8e492b487c935fa502d3de5fb3490 Mon Sep 17 00:00:00 2001 From: Jeremy Yen Date: Thu, 23 Nov 2023 03:54:53 +0800 Subject: [PATCH 3/5] Fix --- banjax-config.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/banjax-config.yaml b/banjax-config.yaml index 706687e..a741e2c 100644 --- a/banjax-config.yaml +++ b/banjax-config.yaml @@ -11,7 +11,9 @@ global_decision_lists: iptables_ban_seconds: 10 iptables_unbanner_seconds: 5 kafka_brokers: - - "localhost:9092" + - kafkadev0.prod.deflect.network:9094 + - kafkadev1.prod.deflect.network:9094 + - kafkadev2.prod.deflect.network:9094 kafka_security_protocol: 'ssl' kafka_ssl_ca: "/etc/banjax/caroot.pem" kafka_ssl_cert: "/etc/banjax/certificate.pem" @@ -98,7 +100,7 @@ hmac_secret: secret gin_log_file: /var/log/banjax/gin.log metrics_log_file: /var/log/banjax/metrics.log debug: true -disable_kafka: true +disable_kafka: false # sha_inv_challenge_html: /etc/banjax/sha-inverse-challenge.html # password_protected_path_html: /etc/banjax/password-protected-path.html disable_logging: @@ -111,4 +113,3 @@ sites_to_disable_baskerville: sub.localhost: false use_user_agent_in_cookie: sub.localhost: true -session_cookie_not_verify: true From 33263ff07628146c4f6596598ace08e407da36f7 Mon Sep 17 00:00:00 2001 From: Jeremy Yen Date: Mon, 27 Nov 2023 17:32:57 +0800 Subject: [PATCH 4/5] Handle empty session and decode err --- internal/kafka.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/kafka.go b/internal/kafka.go index f44e009..73ace8a 100644 --- a/internal/kafka.go +++ b/internal/kafka.go @@ -166,12 +166,20 @@ func handleCommand( } break case "challenge_session": + if command.SessionId == "" { + log.Printf("KAFKA: challenge_session: session_id is EMPTY, break\n") + break + } // exempt a site from challenge according to config _, disabled := config.SitesToDisableBaskerville[command.Host] if !disabled { // gin does urldecode or cookie, so we decode any possible urlencoded session id from kafka - sessionIdDecoded, _ := url.QueryUnescape(command.SessionId) + sessionIdDecoded, decodeErr := url.QueryUnescape(command.SessionId) + if decodeErr != nil { + log.Printf("KAFKA: challenge_session: fail to urldecode session_id %s, break\n", command.SessionId) + break + } updateExpiringDecisionListsSessionId( config, command.Value, From 558b00917737f1aa19527bbab18725e7e15fb5b8 Mon Sep 17 00:00:00 2001 From: Jeremy Yen Date: Mon, 27 Nov 2023 17:42:05 +0800 Subject: [PATCH 5/5] Handle block_session command --- banjax-config.yaml | 2 +- internal/config.go | 2 +- internal/kafka.go | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/banjax-config.yaml b/banjax-config.yaml index a741e2c..e2aab5b 100644 --- a/banjax-config.yaml +++ b/banjax-config.yaml @@ -100,7 +100,7 @@ hmac_secret: secret gin_log_file: /var/log/banjax/gin.log metrics_log_file: /var/log/banjax/metrics.log debug: true -disable_kafka: false +disable_kafka: true # sha_inv_challenge_html: /etc/banjax/sha-inverse-challenge.html # password_protected_path_html: /etc/banjax/password-protected-path.html disable_logging: diff --git a/internal/config.go b/internal/config.go index 71fcf97..3777284 100644 --- a/internal/config.go +++ b/internal/config.go @@ -457,7 +457,7 @@ func updateExpiringDecisionListsSessionId( } if config.Debug { - log.Printf("Update session id challenge with IP %s, session id %s, existing and new: %v, %v\n", + log.Printf("Update session id decision with IP %s, session id %s, existing and new: %v, %v\n", ip, sessionId, existingExpiringDecision.Decision, newDecision) } expires := now.Add(time.Duration(config.ExpiringDecisionTtlSeconds) * time.Second) diff --git a/internal/kafka.go b/internal/kafka.go index 73ace8a..cfec582 100644 --- a/internal/kafka.go +++ b/internal/kafka.go @@ -166,8 +166,9 @@ func handleCommand( } break case "challenge_session": + case "block_session": if command.SessionId == "" { - log.Printf("KAFKA: challenge_session: session_id is EMPTY, break\n") + log.Printf("KAFKA: session_id is EMPTY, break\n") break } // exempt a site from challenge according to config @@ -177,9 +178,17 @@ func handleCommand( // gin does urldecode or cookie, so we decode any possible urlencoded session id from kafka sessionIdDecoded, decodeErr := url.QueryUnescape(command.SessionId) if decodeErr != nil { - log.Printf("KAFKA: challenge_session: fail to urldecode session_id %s, break\n", command.SessionId) + log.Printf("KAFKA: fail to urldecode session_id %s, break\n", command.SessionId) break } + var decision Decision + if command.Name == "block_session" { + log.Printf("KAFKA: block_session: %s\n", sessionIdDecoded) + decision = NginxBlock + } else { + log.Printf("KAFKA: challenge_session: %s\n", sessionIdDecoded) + decision = Challenge + } updateExpiringDecisionListsSessionId( config, command.Value, @@ -187,12 +196,11 @@ func handleCommand( decisionListsMutex, decisionLists, time.Now(), - Challenge, + decision, true, // from baskerville, provide to http_server to distinguish from regex ) - log.Printf("KAFKA: challenge_session: %s\n", sessionIdDecoded) } else { - log.Printf("KAFKA: DIS-BASK: not challenge %s, site %s disabled baskerville\n", command.Value, command.Host) + log.Printf("KAFKA: DIS-BASK: no action on %s, site %s disabled baskerville\n", command.Value, command.Host) } break default: