Files
teleport/lib/utils/net.go
T
Julia Ogris 4df912cce9 limiter: Fix custom rate bucket clobbering (#64828)
* 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.
2026-03-31 01:37:58 +00:00

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]
}