Make trusted proxies configurable and default to loopback / private IPs.

This commit is contained in:
Joachim Bauch 2024-05-16 14:44:00 +02:00
parent 936f83feb9
commit aac4874e72
No known key found for this signature in database
GPG key ID: 77C1D22D53E15F02
7 changed files with 132 additions and 47 deletions

View file

@ -22,6 +22,7 @@
package signaling
import (
"bytes"
"fmt"
"net"
"strings"
@ -31,6 +32,19 @@ type AllowedIps struct {
allowed []*net.IPNet
}
func (a *AllowedIps) String() string {
var b bytes.Buffer
b.WriteString("[")
for idx, n := range a.allowed {
if idx > 0 {
b.WriteString(", ")
}
b.WriteString(n.String())
}
b.WriteString("]")
return b.String()
}
func (a *AllowedIps) Empty() bool {
return len(a.allowed) == 0
}
@ -99,3 +113,22 @@ func DefaultAllowedIps() *AllowedIps {
}
return result
}
var (
privateIpNets = []string{
// Loopback addresses.
"127.0.0.0/8",
// Private addresses.
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
}
)
func DefaultPrivateIps() *AllowedIps {
allowed, err := ParseAllowedIps(strings.Join(privateIpNets, ","))
if err != nil {
panic(fmt.Errorf("could not parse private ips %+v: %w", privateIpNets, err))
}
return allowed
}

View file

@ -881,15 +881,9 @@ func (b *BackendServer) roomHandler(w http.ResponseWriter, r *http.Request, body
}
func (b *BackendServer) allowStatsAccess(r *http.Request) bool {
addr := getRealUserIP(r)
if strings.Contains(addr, ":") {
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
}
}
addr := b.hub.getRealUserIP(r)
ip := net.ParseIP(addr)
if ip == nil {
if len(ip) == 0 {
return false
}

47
hub.go
View file

@ -103,6 +103,8 @@ var (
// Delay after which a "cleared" / "rejected" dialout status should be removed.
removeCallStatusTTL = 5 * time.Second
DefaultTrustedProxies = DefaultPrivateIps()
)
const (
@ -163,6 +165,7 @@ type Hub struct {
backendTimeout time.Duration
backend *BackendClient
trustedProxies *AllowedIps
geoip *GeoLookup
geoipOverrides map[*net.IPNet]string
geoipUpdating atomic.Bool
@ -226,6 +229,19 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer
log.Printf("WARNING: Allow subscribing any streams, this is insecure and should only be enabled for testing")
}
trustedProxies, _ := config.GetString("app", "trustedproxies")
trustedProxiesIps, err := ParseAllowedIps(trustedProxies)
if err != nil {
return nil, err
}
if !trustedProxiesIps.Empty() {
log.Printf("Trusted proxies: %s", trustedProxiesIps)
} else {
trustedProxiesIps = DefaultTrustedProxies
log.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps)
}
decodeCaches := make([]*LruCache, 0, numDecodeCaches)
for i := 0; i < numDecodeCaches; i++ {
decodeCaches = append(decodeCaches, NewLruCache(decodeCacheSize))
@ -353,6 +369,7 @@ func NewHub(config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer
backendTimeout: backendTimeout,
backend: backend,
trustedProxies: trustedProxiesIps,
geoip: geoip,
geoipOverrides: geoipOverrides,
@ -2512,9 +2529,21 @@ func (h *Hub) GetStats() map[string]interface{} {
return result
}
func getRealUserIP(r *http.Request) string {
// Note this function assumes it is running behind a trusted proxy, so
// the headers can be trusted.
func GetRealUserIP(r *http.Request, trusted *AllowedIps) string {
addr := r.RemoteAddr
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
}
ip := net.ParseIP(addr)
if len(ip) == 0 {
return addr
}
if trusted == nil || !trusted.Allowed(ip) {
return addr
}
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
@ -2524,14 +2553,22 @@ func getRealUserIP(r *http.Request) string {
if pos := strings.Index(ip, ","); pos >= 0 {
ip = strings.TrimSpace(ip[:pos])
}
// Make sure to remove any port.
if host, _, err := net.SplitHostPort(ip); err == nil {
ip = host
}
return ip
}
return r.RemoteAddr
return addr
}
func (h *Hub) getRealUserIP(r *http.Request) string {
return GetRealUserIP(r, h.trustedProxies)
}
func (h *Hub) serveWs(w http.ResponseWriter, r *http.Request) {
addr := getRealUserIP(r)
addr := h.getRealUserIP(r)
agent := r.Header.Get("User-Agent")
conn, err := h.upgrader.Upgrade(w, r, nil)

View file

@ -3580,10 +3580,15 @@ func TestJoinRoomSwitchClient(t *testing.T) {
func TestGetRealUserIP(t *testing.T) {
REMOTE_ATTR := "192.168.1.2"
request := &http.Request{
RemoteAddr: REMOTE_ATTR,
trustedProxies, err := ParseAllowedIps("192.168.0.0/16")
if err != nil {
t.Fatal(err)
}
if ip := getRealUserIP(request); ip != REMOTE_ATTR {
request := &http.Request{
RemoteAddr: REMOTE_ATTR + ":23456",
}
if ip := GetRealUserIP(request, trustedProxies); ip != REMOTE_ATTR {
t.Errorf("Expected %s but got %s", REMOTE_ATTR, ip)
}
@ -3591,27 +3596,42 @@ func TestGetRealUserIP(t *testing.T) {
request.Header = http.Header{
http.CanonicalHeaderKey("x-real-ip"): []string{X_REAL_IP},
}
if ip := getRealUserIP(request); ip != X_REAL_IP {
if ip := GetRealUserIP(request, trustedProxies); ip != X_REAL_IP {
t.Errorf("Expected %s but got %s", X_REAL_IP, ip)
}
// "X-Real-IP" has preference before "X-Forwarded-For"
X_FORWARDED_FOR_IP := "192.168.20.21"
X_FORWARDED_FOR := X_FORWARDED_FOR_IP + ", 192.168.30.32"
X_FORWARDED_FOR := X_FORWARDED_FOR_IP + ":12345, 192.168.30.32"
request.Header = http.Header{
http.CanonicalHeaderKey("x-real-ip"): []string{X_REAL_IP},
http.CanonicalHeaderKey("x-forwarded-for"): []string{X_FORWARDED_FOR},
}
if ip := getRealUserIP(request); ip != X_REAL_IP {
if ip := GetRealUserIP(request, trustedProxies); ip != X_REAL_IP {
t.Errorf("Expected %s but got %s", X_REAL_IP, ip)
}
request.Header = http.Header{
http.CanonicalHeaderKey("x-forwarded-for"): []string{X_FORWARDED_FOR},
}
if ip := getRealUserIP(request); ip != X_FORWARDED_FOR_IP {
if ip := GetRealUserIP(request, trustedProxies); ip != X_FORWARDED_FOR_IP {
t.Errorf("Expected %s but got %s", X_FORWARDED_FOR_IP, ip)
}
PUBLIC_IP := "1.2.3.4"
request.RemoteAddr = PUBLIC_IP + ":1234"
request.Header = http.Header{
http.CanonicalHeaderKey("x-real-ip"): []string{X_REAL_IP},
}
if ip := GetRealUserIP(request, trustedProxies); ip != PUBLIC_IP {
t.Errorf("Expected %s but got %s", PUBLIC_IP, ip)
}
request.Header = http.Header{
http.CanonicalHeaderKey("x-forwarded-for"): []string{X_FORWARDED_FOR},
}
if ip := GetRealUserIP(request, trustedProxies); ip != PUBLIC_IP {
t.Errorf("Expected %s but got %s", PUBLIC_IP, ip)
}
}
func TestClientMessageToSessionIdWhileDisconnected(t *testing.T) {

View file

@ -8,6 +8,11 @@
# See "https://golang.org/pkg/net/http/pprof/" for further information.
#debug = false
# Comma separated list of trusted proxies (IPs or CIDR networks) that may set
# the "X-Real-Ip" or "X-Forwarded-For" headers.
# Leave empty to allow loopback and local addresses.
#trustedproxies =
# ISO 3166 country this proxy is located at. This will be used by the signaling
# servers to determine the closest proxy for publishers.
#country = DE

View file

@ -99,6 +99,7 @@ type ProxyServer struct {
tokens ProxyTokens
statsAllowedIps *signaling.AllowedIps
trustedProxies *signaling.AllowedIps
sid atomic.Uint64
cookie *securecookie.SecureCookie
@ -153,6 +154,19 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile) (*
statsAllowedIps = signaling.DefaultAllowedIps()
}
trustedProxies, _ := config.GetString("app", "trustedproxies")
trustedProxiesIps, err := signaling.ParseAllowedIps(trustedProxies)
if err != nil {
return nil, err
}
if !trustedProxiesIps.Empty() {
log.Printf("Trusted proxies: %s", trustedProxiesIps)
} else {
trustedProxiesIps = signaling.DefaultTrustedProxies
log.Printf("No trusted proxies configured, only allowing for %s", trustedProxiesIps)
}
country, _ := config.GetString("app", "country")
country = strings.ToUpper(country)
if signaling.IsValidCountry(country) {
@ -187,6 +201,7 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile) (*
tokens: tokens,
statsAllowedIps: statsAllowedIps,
trustedProxies: trustedProxiesIps,
cookie: securecookie.New(hashKey, blockKey).MaxAge(0),
sessions: make(map[uint64]*ProxySession),
@ -398,24 +413,6 @@ func (s *ProxyServer) setCommonHeaders(f func(http.ResponseWriter, *http.Request
}
}
func getRealUserIP(r *http.Request) string {
// Note this function assumes it is running behind a trusted proxy, so
// the headers can be trusted.
if ip := r.Header.Get("X-Real-IP"); ip != "" {
return ip
}
if ip := r.Header.Get("X-Forwarded-For"); ip != "" {
// Result could be a list "clientip, proxy1, proxy2", so only use first element.
if pos := strings.Index(ip, ","); pos >= 0 {
ip = strings.TrimSpace(ip[:pos])
}
return ip
}
return r.RemoteAddr
}
func (s *ProxyServer) welcomeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
@ -423,7 +420,7 @@ func (s *ProxyServer) welcomeHandler(w http.ResponseWriter, r *http.Request) {
}
func (s *ProxyServer) proxyHandler(w http.ResponseWriter, r *http.Request) {
addr := getRealUserIP(r)
addr := signaling.GetRealUserIP(r, s.trustedProxies)
conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("Could not upgrade request from %s: %s", addr, err)
@ -1018,15 +1015,9 @@ func (s *ProxyServer) getStats() map[string]interface{} {
}
func (s *ProxyServer) allowStatsAccess(r *http.Request) bool {
addr := getRealUserIP(r)
if strings.Contains(addr, ":") {
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
}
}
addr := signaling.GetRealUserIP(r, s.trustedProxies)
ip := net.ParseIP(addr)
if ip == nil {
if len(ip) == 0 {
return false
}

View file

@ -34,6 +34,11 @@ debug = false
# room and call can be subscribed.
#allowsubscribeany = false
# Comma separated list of trusted proxies (IPs or CIDR networks) that may set
# the "X-Real-Ip" or "X-Forwarded-For" headers.
# Leave empty to allow loopback and local addresses.
#trustedproxies =
[sessions]
# Secret value used to generate checksums of sessions. This should be a random
# string of 32 or 64 bytes.