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 package signaling
import ( import (
"bytes"
"fmt" "fmt"
"net" "net"
"strings" "strings"
@ -31,6 +32,19 @@ type AllowedIps struct {
allowed []*net.IPNet 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 { func (a *AllowedIps) Empty() bool {
return len(a.allowed) == 0 return len(a.allowed) == 0
} }
@ -99,3 +113,22 @@ func DefaultAllowedIps() *AllowedIps {
} }
return result 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 { func (b *BackendServer) allowStatsAccess(r *http.Request) bool {
addr := getRealUserIP(r) addr := b.hub.getRealUserIP(r)
if strings.Contains(addr, ":") {
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
}
}
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
if ip == nil { if len(ip) == 0 {
return false return false
} }

47
hub.go
View file

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

View file

@ -3580,10 +3580,15 @@ func TestJoinRoomSwitchClient(t *testing.T) {
func TestGetRealUserIP(t *testing.T) { func TestGetRealUserIP(t *testing.T) {
REMOTE_ATTR := "192.168.1.2" REMOTE_ATTR := "192.168.1.2"
request := &http.Request{ trustedProxies, err := ParseAllowedIps("192.168.0.0/16")
RemoteAddr: REMOTE_ATTR, 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) t.Errorf("Expected %s but got %s", REMOTE_ATTR, ip)
} }
@ -3591,27 +3596,42 @@ func TestGetRealUserIP(t *testing.T) {
request.Header = http.Header{ request.Header = http.Header{
http.CanonicalHeaderKey("x-real-ip"): []string{X_REAL_IP}, 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) t.Errorf("Expected %s but got %s", X_REAL_IP, ip)
} }
// "X-Real-IP" has preference before "X-Forwarded-For" // "X-Real-IP" has preference before "X-Forwarded-For"
X_FORWARDED_FOR_IP := "192.168.20.21" 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{ request.Header = http.Header{
http.CanonicalHeaderKey("x-real-ip"): []string{X_REAL_IP}, http.CanonicalHeaderKey("x-real-ip"): []string{X_REAL_IP},
http.CanonicalHeaderKey("x-forwarded-for"): []string{X_FORWARDED_FOR}, 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) t.Errorf("Expected %s but got %s", X_REAL_IP, ip)
} }
request.Header = http.Header{ request.Header = http.Header{
http.CanonicalHeaderKey("x-forwarded-for"): []string{X_FORWARDED_FOR}, 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) 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) { func TestClientMessageToSessionIdWhileDisconnected(t *testing.T) {

View file

@ -8,6 +8,11 @@
# See "https://golang.org/pkg/net/http/pprof/" for further information. # See "https://golang.org/pkg/net/http/pprof/" for further information.
#debug = false #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 # ISO 3166 country this proxy is located at. This will be used by the signaling
# servers to determine the closest proxy for publishers. # servers to determine the closest proxy for publishers.
#country = DE #country = DE

View file

@ -99,6 +99,7 @@ type ProxyServer struct {
tokens ProxyTokens tokens ProxyTokens
statsAllowedIps *signaling.AllowedIps statsAllowedIps *signaling.AllowedIps
trustedProxies *signaling.AllowedIps
sid atomic.Uint64 sid atomic.Uint64
cookie *securecookie.SecureCookie cookie *securecookie.SecureCookie
@ -153,6 +154,19 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile) (*
statsAllowedIps = signaling.DefaultAllowedIps() 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, _ := config.GetString("app", "country")
country = strings.ToUpper(country) country = strings.ToUpper(country)
if signaling.IsValidCountry(country) { if signaling.IsValidCountry(country) {
@ -187,6 +201,7 @@ func NewProxyServer(r *mux.Router, version string, config *goconf.ConfigFile) (*
tokens: tokens, tokens: tokens,
statsAllowedIps: statsAllowedIps, statsAllowedIps: statsAllowedIps,
trustedProxies: trustedProxiesIps,
cookie: securecookie.New(hashKey, blockKey).MaxAge(0), cookie: securecookie.New(hashKey, blockKey).MaxAge(0),
sessions: make(map[uint64]*ProxySession), 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) { func (s *ProxyServer) welcomeHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK) 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) { 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) conn, err := s.upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Printf("Could not upgrade request from %s: %s", addr, err) 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 { func (s *ProxyServer) allowStatsAccess(r *http.Request) bool {
addr := getRealUserIP(r) addr := signaling.GetRealUserIP(r, s.trustedProxies)
if strings.Contains(addr, ":") {
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
}
}
ip := net.ParseIP(addr) ip := net.ParseIP(addr)
if ip == nil { if len(ip) == 0 {
return false return false
} }

View file

@ -34,6 +34,11 @@ debug = false
# room and call can be subscribed. # room and call can be subscribed.
#allowsubscribeany = false #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] [sessions]
# Secret value used to generate checksums of sessions. This should be a random # Secret value used to generate checksums of sessions. This should be a random
# string of 32 or 64 bytes. # string of 32 or 64 bytes.