mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-21 14:35:22 +08:00
* limiter: Add failing test for custom rate bucket clobbering Add an assertion to TestCustomRate that proves the bug: when a default-rate RegisterRequest call interleaves with custom-rate calls for the same client IP, the custom-rate bucket is clobbered because both share a single TokenBucketSet keyed by IP alone. The test fails on master at limiter_test.go:145. The next commit fixes the bug. * limiter: Use dedicated RateLimiter for account recovery Replace the shared-bucket custom rate approach with a separate RateLimiter instance for account recovery RPCs. This follows the same pattern as gravitational/teleport#64559 and avoids the shared-bucket clobber problem entirely: each concern gets its own RateLimiter with its own FnCache, so default-rate requests cannot reset stricter per-endpoint buckets. The `getCustomRate` middleware passed per-endpoint rate sets through a shared FnCache keyed only by client IP. When a default-rate request hit the same IP, `TokenBucketSet.Update` overwrote the cached bucket with the default rate parameters, resetting the stricter custom-rate state. Add a private `accountRecoveryLimiter` to the auth Middleware and wire it into `rateLimitUnaryInterceptor`, which applies the default limiter to all endpoints and the recovery limiter on top. Remove `getCustomRate`, `CustomRateFunc`, `UnaryServerInterceptorWithCustomRate`, and the `customRate` parameter from `RateLimiter.RegisterRequest` and `RegisterRequestFromAddr`. Export `ClientIPFromContext` from the limiter package for use by the middleware. Keep a deprecated `Limiter.RegisterRequestWithCustomRate` shim for backwards compatibility with enterprise callers (they all pass nil). * limiter: Remove stale accountRecoveryEndpoints comment Remove orphaned comment from the deleted `accountRecoveryEndpoints` variable. The comment was left behind when the variable was replaced by inline switch cases in `rateLimitUnaryInterceptor`. * limiter: Guard nil accountRecoveryLimiter in interceptor The accountRecoveryLimiter is nil when Middleware is constructed without calling newAccountRecoveryLimiter, which happens in initSecureGRPCServer for Kube-only traffic. The nil dereference is not reachable on current code paths because recovery RPCs are never routed to that server, but add a nil guard to make the interceptor safe independently of how the server is wired. * limiter: Remove RegisterRequestWithCustomRate Remove the backwards-compatibility shim now that all enterprise callers have been removed via gravitational/teleport.e#8359. * limiter: Extract ClientIPFromAddr to lib/utils Move the addr-to-IP parsing into `utils.ClientIPFromAddr` (next to `ClientIPFromConn`) so `lib/auth` does not depend on `lib/limiter` for IP extraction. Add an unexported `clientIPFromContext` in `lib/auth` for the gRPC peer lookup. Reduce the public API surface of `lib/limiter` that deals with gRPC concepts, keeping it closer to its intended role as a general-purpose rate limiter keyed by string tokens.
94 lines
3.2 KiB
Go
94 lines
3.2 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 Gravitational, Inc.
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package utils
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
|
|
"github.com/gravitational/trace"
|
|
)
|
|
|
|
// ClientIPFromAddr extracts the client IP from a network address.
|
|
// For bufconn test addresses it returns the literal string "bufconn".
|
|
func ClientIPFromAddr(addr net.Addr) (string, error) {
|
|
if addr == nil {
|
|
return "", trace.BadParameter("missing client IP")
|
|
}
|
|
s := addr.String()
|
|
// bufconn peers don't include host:port, so use a stable synthetic
|
|
// key for request/connection limiting in tests.
|
|
if s == "bufconn" && addr.Network() == "bufconn" {
|
|
return "bufconn", nil
|
|
}
|
|
clientIP, _, err := net.SplitHostPort(s)
|
|
if err != nil {
|
|
return "", trace.BadParameter("missing client IP")
|
|
}
|
|
return clientIP, nil
|
|
}
|
|
|
|
// ClientIPFromConn extracts host from provided remote address.
|
|
func ClientIPFromConn(conn net.Conn) (string, error) {
|
|
clientRemoteAddr := conn.RemoteAddr()
|
|
|
|
clientIP, _, err := net.SplitHostPort(clientRemoteAddr.String())
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
|
|
return clientIP, nil
|
|
}
|
|
|
|
// FindMatchingProxyDNS checks if a given request host or app fqdn matches any of the specified proxy DNS names.
|
|
// It compares the hostnames without considering the port numbers.
|
|
// If a match is found, the method returns the original proxy DNS name (including its port if present).
|
|
// If no match is found, it returns the first proxy DNS name from the list.
|
|
//
|
|
// Parameters:
|
|
// - requestHostnameOrFQDN: A string representing the host in the request, which may include a port.
|
|
// - proxyDNSNames: A slice of strings representing possible DNS names for a proxy, each of which may include a port.
|
|
//
|
|
// Returns:
|
|
// - A string representing the matching proxy DNS name with its port, or the first proxy DNS name if no matches are found.
|
|
func FindMatchingProxyDNS(requestHostnameOrFQDN string, proxyDNSNames []string) string {
|
|
if requestHostnameOrFQDN == "" || len(proxyDNSNames) == 0 {
|
|
return ""
|
|
}
|
|
|
|
// Remove port from request host if present.
|
|
normalizedRequestHost := strings.Split(requestHostnameOrFQDN, ":")[0]
|
|
hostParts := strings.Split(normalizedRequestHost, ".")
|
|
|
|
// Iterate over each possible suffix of requestHostOrFQDN parts
|
|
for start := range hostParts {
|
|
possibleHost := strings.Join(hostParts[start:], ".")
|
|
for _, proxyDNSName := range proxyDNSNames {
|
|
// Normalize proxy DNS name by removing port if present
|
|
normalizedProxyDNSName := strings.Split(proxyDNSName, ":")[0]
|
|
if possibleHost == normalizedProxyDNSName {
|
|
return proxyDNSName
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no match found, return the first proxyDNSName as fallback
|
|
return proxyDNSNames[0]
|
|
}
|