* Bump the go-modules-updates group in /backend with 7 updates Bumps the go-modules-updates group in /backend with 7 updates: | Package | From | To | | --- | --- | --- | | [github.com/alecthomas/chroma/v2](https://github.com/alecthomas/chroma) | `2.21.1` | `2.23.1` | | [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) | `5.2.3` | `5.2.4` | | [github.com/go-pkgz/rest](https://github.com/go-pkgz/rest) | `1.20.6` | `1.21.0` | | [github.com/golang-jwt/jwt/v5](https://github.com/golang-jwt/jwt) | `5.3.0` | `5.3.1` | | [golang.org/x/crypto](https://github.com/golang/crypto) | `0.46.0` | `0.47.0` | | [golang.org/x/image](https://github.com/golang/image) | `0.34.0` | `0.35.0` | | [golang.org/x/net](https://github.com/golang/net) | `0.48.0` | `0.49.0` | Updates `github.com/alecthomas/chroma/v2` from 2.21.1 to 2.23.1 - [Release notes](https://github.com/alecthomas/chroma/releases) - [Commits](https://github.com/alecthomas/chroma/compare/v2.21.1...v2.23.1) Updates `github.com/go-chi/chi/v5` from 5.2.3 to 5.2.4 - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.3...v5.2.4) Updates `github.com/go-pkgz/rest` from 1.20.6 to 1.21.0 - [Release notes](https://github.com/go-pkgz/rest/releases) - [Commits](https://github.com/go-pkgz/rest/compare/v1.20.6...v1.21.0) Updates `github.com/golang-jwt/jwt/v5` from 5.3.0 to 5.3.1 - [Release notes](https://github.com/golang-jwt/jwt/releases) - [Commits](https://github.com/golang-jwt/jwt/compare/v5.3.0...v5.3.1) Updates `golang.org/x/crypto` from 0.46.0 to 0.47.0 - [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0) Updates `golang.org/x/image` from 0.34.0 to 0.35.0 - [Commits](https://github.com/golang/image/compare/v0.34.0...v0.35.0) Updates `golang.org/x/net` from 0.48.0 to 0.49.0 - [Commits](https://github.com/golang/net/compare/v0.48.0...v0.49.0) --- updated-dependencies: - dependency-name: github.com/alecthomas/chroma/v2 dependency-version: 2.23.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.2.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: github.com/go-pkgz/rest dependency-version: 1.21.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: github.com/golang-jwt/jwt/v5 dependency-version: 5.3.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: go-modules-updates - dependency-name: golang.org/x/crypto dependency-version: 0.47.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/image dependency-version: 0.35.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates - dependency-name: golang.org/x/net dependency-version: 0.49.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: go-modules-updates ... Signed-off-by: dependabot[bot] <support@github.com> * Run go mod tidy in examples directory Co-authored-by: paskal <712534+paskal@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: paskal <712534+paskal@users.noreply.github.com>
121 lines
3.8 KiB
Go
121 lines
3.8 KiB
Go
// Package realip extracts a real IP address from the request.
|
|
package realip
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type ipRange struct {
|
|
start net.IP
|
|
end net.IP
|
|
}
|
|
|
|
// privateRanges contains the list of private and special-use IP ranges.
|
|
// reference: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
|
|
var privateRanges = []ipRange{
|
|
// IPv4 Private Ranges
|
|
{start: net.ParseIP("10.0.0.0"), end: net.ParseIP("10.255.255.255")},
|
|
{start: net.ParseIP("172.16.0.0"), end: net.ParseIP("172.31.255.255")},
|
|
{start: net.ParseIP("192.168.0.0"), end: net.ParseIP("192.168.255.255")},
|
|
// IPv4 Link-Local
|
|
{start: net.ParseIP("169.254.0.0"), end: net.ParseIP("169.254.255.255")},
|
|
// IPv4 Shared Address Space (RFC 6598)
|
|
{start: net.ParseIP("100.64.0.0"), end: net.ParseIP("100.127.255.255")},
|
|
// IPv4 Benchmarking (RFC 2544)
|
|
{start: net.ParseIP("198.18.0.0"), end: net.ParseIP("198.19.255.255")},
|
|
// IPv6 Unique Local Addresses (ULA)
|
|
{start: net.ParseIP("fc00::"), end: net.ParseIP("fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")},
|
|
// IPv6 Link-local Addresses
|
|
{start: net.ParseIP("fe80::"), end: net.ParseIP("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")},
|
|
}
|
|
|
|
// Get returns real IP from the given request.
|
|
// It checks headers in the following priority order:
|
|
// 1. X-Real-IP - trusted proxy (nginx/reproxy) sets this to actual client
|
|
// 2. CF-Connecting-IP - Cloudflare's header for original client
|
|
// 3. X-Forwarded-For - leftmost public IP (original client in CDN chain)
|
|
// 4. RemoteAddr - fallback for direct connections
|
|
//
|
|
// Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped.
|
|
func Get(r *http.Request) (string, error) {
|
|
// check X-Real-IP first (single value, set by trusted proxy)
|
|
if xRealIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); xRealIP != "" {
|
|
if ip := net.ParseIP(xRealIP); isPublicIP(ip) {
|
|
return xRealIP, nil
|
|
}
|
|
}
|
|
|
|
// check CF-Connecting-IP (Cloudflare's header)
|
|
if cfIP := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")); cfIP != "" {
|
|
if ip := net.ParseIP(cfIP); isPublicIP(ip) {
|
|
return cfIP, nil
|
|
}
|
|
}
|
|
|
|
// check X-Forwarded-For, find leftmost public IP
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
for addr := range strings.SplitSeq(xff, ",") {
|
|
ip := strings.TrimSpace(addr)
|
|
if parsedIP := net.ParseIP(ip); isPublicIP(parsedIP) {
|
|
return ip, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
// fall back to RemoteAddr
|
|
return parseRemoteAddr(r.RemoteAddr)
|
|
}
|
|
|
|
// isPublicIP checks if the IP is a valid public (globally routable) IP address.
|
|
func isPublicIP(ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
if !ip.IsGlobalUnicast() {
|
|
return false
|
|
}
|
|
return !isPrivateSubnet(ip)
|
|
}
|
|
|
|
// parseRemoteAddr extracts and validates IP from RemoteAddr (handles both "ip" and "ip:port" formats).
|
|
func parseRemoteAddr(remoteAddr string) (string, error) {
|
|
if remoteAddr == "" {
|
|
return "", fmt.Errorf("empty remote address")
|
|
}
|
|
|
|
// try to extract host from host:port format
|
|
host, _, err := net.SplitHostPort(remoteAddr)
|
|
if err == nil {
|
|
remoteAddr = host
|
|
}
|
|
|
|
// validate it's a proper IP address
|
|
if netIP := net.ParseIP(remoteAddr); netIP == nil {
|
|
return "", fmt.Errorf("no valid ip found in %q", remoteAddr)
|
|
}
|
|
|
|
return remoteAddr, nil
|
|
}
|
|
|
|
// isPrivateSubnet - check to see if this ip is in a private subnet
|
|
func isPrivateSubnet(ipAddress net.IP) bool {
|
|
inRange := func(r ipRange, ipAddress net.IP) bool { // check to see if a given ip address is within a range given
|
|
// ensure the IPs are in the same format for comparison
|
|
ipAddress = ipAddress.To16()
|
|
r.start = r.start.To16()
|
|
r.end = r.end.To16()
|
|
return bytes.Compare(ipAddress, r.start) >= 0 && bytes.Compare(ipAddress, r.end) <= 0
|
|
}
|
|
|
|
for _, r := range privateRanges {
|
|
if inRange(r, ipAddress) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|