ingress_reject.go 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167
  1. package middleware
  2. import (
  3. "math"
  4. "net/netip"
  5. "strconv"
  6. "strings"
  7. "sync/atomic"
  8. "time"
  9. "github.com/gin-gonic/gin"
  10. )
  11. // IngressRejectReason identifies expected gateway admission failures that must
  12. // not be treated as operational request errors.
  13. type IngressRejectReason string
  14. const (
  15. IngressRejectQueryAPIKeyDeprecated IngressRejectReason = "query_api_key_deprecated"
  16. IngressRejectAPIKeyRequired IngressRejectReason = "api_key_required"
  17. IngressRejectInvalidAPIKey IngressRejectReason = "invalid_api_key"
  18. IngressRejectAPIKeyDisabled IngressRejectReason = "api_key_disabled"
  19. IngressRejectIPRestricted IngressRejectReason = "ip_restricted"
  20. IngressRejectUserInactive IngressRejectReason = "user_inactive"
  21. IngressRejectGroupDeleted IngressRejectReason = "group_deleted"
  22. IngressRejectGroupDisabled IngressRejectReason = "group_disabled"
  23. IngressRejectGroupNotAllowed IngressRejectReason = "group_not_allowed"
  24. IngressRejectGroupUnassigned IngressRejectReason = "group_unassigned"
  25. IngressRejectInvalidAuthRateLimited IngressRejectReason = "invalid_auth_rate_limited"
  26. IngressRejectAPIKeyAuthOverloaded IngressRejectReason = "api_key_auth_overloaded"
  27. IngressRejectModelNotAllowed IngressRejectReason = "model_not_allowed"
  28. )
  29. const ingressRejectReasonContextKey = "ingress_reject_reason"
  30. type IngressRejectRecorder interface {
  31. RecordIngressReject(reason, routeFamily, protocol, clientIP string, userID, apiKeyID int64)
  32. }
  33. func invalidAuthClientKey(c *gin.Context) string {
  34. return normalizeIngressRejectIP(SecurityClientIP(c))
  35. }
  36. func rejectInvalidAuthAbuse(c *gin.Context, apiKeyService interface {
  37. CheckInvalidAuthAbuse(string) (time.Duration, bool)
  38. }) bool {
  39. if c == nil || apiKeyService == nil {
  40. return false
  41. }
  42. retry, blocked := apiKeyService.CheckInvalidAuthAbuse(invalidAuthClientKey(c))
  43. if !blocked {
  44. return false
  45. }
  46. retrySeconds := int(math.Ceil(retry.Seconds()))
  47. if retrySeconds < 1 {
  48. retrySeconds = 1
  49. }
  50. c.Header("Retry-After", strconv.Itoa(retrySeconds))
  51. MarkIngressRejected(c, IngressRejectInvalidAuthRateLimited)
  52. return true
  53. }
  54. func recordInvalidAuthFailure(c *gin.Context, apiKeyService interface {
  55. RecordInvalidAuthFailure(string)
  56. }) {
  57. if c == nil || apiKeyService == nil {
  58. return
  59. }
  60. apiKeyService.RecordInvalidAuthFailure(invalidAuthClientKey(c))
  61. }
  62. type ingressRejectRecorderHolder struct{ recorder IngressRejectRecorder }
  63. var activeIngressRejectRecorder atomic.Pointer[ingressRejectRecorderHolder]
  64. func SetIngressRejectRecorder(recorder IngressRejectRecorder) {
  65. if recorder == nil {
  66. activeIngressRejectRecorder.Store(nil)
  67. return
  68. }
  69. activeIngressRejectRecorder.Store(&ingressRejectRecorderHolder{recorder: recorder})
  70. }
  71. // MarkIngressRejected marks a request as rejected before gateway admission.
  72. func MarkIngressRejected(c *gin.Context, reason IngressRejectReason) {
  73. if c == nil || reason == "" {
  74. return
  75. }
  76. c.Set(ingressRejectReasonContextKey, reason)
  77. }
  78. // GetIngressRejectReason returns the admission rejection reason, if any.
  79. func GetIngressRejectReason(c *gin.Context) (IngressRejectReason, bool) {
  80. if c == nil {
  81. return "", false
  82. }
  83. value, exists := c.Get(ingressRejectReasonContextKey)
  84. if !exists {
  85. return "", false
  86. }
  87. reason, ok := value.(IngressRejectReason)
  88. return reason, ok && reason != ""
  89. }
  90. func recordIngressReject(c *gin.Context, reason IngressRejectReason) {
  91. holder := activeIngressRejectRecorder.Load()
  92. if holder == nil || holder.recorder == nil || c == nil || c.Request == nil {
  93. return
  94. }
  95. routeFamily, protocol := ingressRejectRoute(c.Request.URL.Path)
  96. clientIP := normalizeIngressRejectIP(SecurityClientIP(c))
  97. var userID, apiKeyID int64
  98. if apiKey, ok := GetAPIKeyFromContext(c); ok && apiKey != nil {
  99. apiKeyID = apiKey.ID
  100. if apiKey.User != nil {
  101. userID = apiKey.User.ID
  102. }
  103. } else if apiKey, ok := GetOpsFallbackAPIKey(c); ok && apiKey != nil {
  104. apiKeyID = apiKey.ID
  105. if apiKey.User != nil {
  106. userID = apiKey.User.ID
  107. }
  108. }
  109. holder.recorder.RecordIngressReject(string(reason), routeFamily, protocol, clientIP, userID, apiKeyID)
  110. }
  111. func normalizeIngressRejectIP(raw string) string {
  112. addr, err := netip.ParseAddr(strings.TrimSpace(raw))
  113. if err != nil {
  114. return "0.0.0.0"
  115. }
  116. addr = addr.Unmap()
  117. if addr.Is6() {
  118. return netip.PrefixFrom(addr, 64).Masked().Addr().String()
  119. }
  120. return addr.String()
  121. }
  122. func ingressRejectRoute(path string) (string, string) {
  123. path = strings.ToLower(strings.TrimSpace(path))
  124. switch {
  125. case strings.HasPrefix(path, "/antigravity/v1beta"):
  126. return "antigravity", "google"
  127. case strings.HasPrefix(path, "/v1beta"):
  128. return "gemini", "google"
  129. case strings.HasPrefix(path, "/backend-api/codex"):
  130. return "codex", "openai"
  131. case strings.HasPrefix(path, "/antigravity"):
  132. return "antigravity", "anthropic"
  133. case strings.Contains(path, "/messages"):
  134. return "messages", "anthropic"
  135. case strings.Contains(path, "/responses"):
  136. return "responses", "openai"
  137. case strings.Contains(path, "/chat/completions"):
  138. return "chat_completions", "openai"
  139. case strings.Contains(path, "/images"):
  140. return "images", "openai"
  141. case strings.Contains(path, "/videos"):
  142. return "videos", "openai"
  143. case strings.Contains(path, "/embeddings"):
  144. return "embeddings", "openai"
  145. case strings.Contains(path, "/models"):
  146. return "models", "openai"
  147. default:
  148. return "other", "gateway"
  149. }
  150. }