Files
dependabot[bot]GitHubpaskaldependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
638fa63e81 Bump the go-modules-updates group in /backend with 7 updates (#1995)
* 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>
2026-02-14 19:48:24 -06:00

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
}