Move IPList (from AllowedIps) to container package.

This commit is contained in:
Joachim Bauch 2025-12-10 16:26:33 +01:00
commit bc9b353975
No known key found for this signature in database
GPG key ID: 77C1D22D53E15F02
10 changed files with 109 additions and 75 deletions

View file

@ -30,6 +30,10 @@ component_management:
name: client
paths:
- client/**
- component_id: module_container
name: container
paths:
- container/**
- component_id: module_internal
name: internal
paths:

View file

@ -37,6 +37,7 @@ import (
"github.com/pion/sdp/v3"
"github.com/strukturag/nextcloud-spreed-signaling/api"
"github.com/strukturag/nextcloud-spreed-signaling/container"
"github.com/strukturag/nextcloud-spreed-signaling/internal"
)
@ -838,7 +839,7 @@ func (m *MessageClientMessageData) CheckValid() error {
return nil
}
func FilterCandidate(c ice.Candidate, allowed *AllowedIps, blocked *AllowedIps) bool {
func FilterCandidate(c ice.Candidate, allowed *container.IPList, blocked *container.IPList) bool {
switch c {
case nil:
return true
@ -852,19 +853,19 @@ func FilterCandidate(c ice.Candidate, allowed *AllowedIps, blocked *AllowedIps)
}
// Whitelist has preference.
if allowed != nil && allowed.Allowed(ip) {
if allowed != nil && allowed.Contains(ip) {
return false
}
// Check if address is blocked manually.
if blocked != nil && blocked.Allowed(ip) {
if blocked != nil && blocked.Contains(ip) {
return true
}
return false
}
func FilterSDPCandidates(s *sdp.SessionDescription, allowed *AllowedIps, blocked *AllowedIps) bool {
func FilterSDPCandidates(s *sdp.SessionDescription, allowed *container.IPList, blocked *container.IPList) bool {
modified := false
for _, m := range s.MediaDescriptions {
m.Attributes = slices.DeleteFunc(m.Attributes, func(a sdp.Attribute) bool {

View file

@ -31,6 +31,8 @@ import (
"github.com/pion/ice/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/strukturag/nextcloud-spreed-signaling/container"
)
type testCheckValid interface {
@ -481,17 +483,17 @@ func TestFilterCandidates(t *testing.T) {
continue
}
var allowed *AllowedIps
var allowed *container.IPList
if tc.allowed != "" {
allowed, err = ParseAllowedIps(tc.allowed)
allowed, err = container.ParseIPList(tc.allowed)
if !assert.NoError(err, "parsing allowed list %s failed in testcase %d", tc.allowed, idx) {
continue
}
}
var blocked *AllowedIps
var blocked *container.IPList
if tc.blocked != "" {
blocked, err = ParseAllowedIps(tc.blocked)
blocked, err = container.ParseIPList(tc.blocked)
if !assert.NoError(err, "parsing blocked list %s failed in testcase %d", tc.blocked, idx) {
continue
}
@ -527,7 +529,7 @@ func TestFilterSDPCandidates(t *testing.T) {
assert.Equal(expectedBefore[m.MediaName.Media], count, "invalid number of candidates for media description %s", m.MediaName.Media)
}
blocked, err := ParseAllowedIps("192.0.0.0/24, 192.168.0.0/16")
blocked, err := container.ParseIPList("192.0.0.0/24, 192.168.0.0/16")
require.NoError(err)
expectedAfter := map[string]int{
@ -577,7 +579,7 @@ func TestNoFilterSDPCandidates(t *testing.T) {
assert.Equal(expectedBefore[m.MediaName.Media], count, "invalid number of candidates for media description %s", m.MediaName.Media)
}
blocked, err := ParseAllowedIps("192.0.0.0/24, 192.168.0.0/16")
blocked, err := container.ParseIPList("192.0.0.0/24, 192.168.0.0/16")
require.NoError(err)
expectedAfter := map[string]int{

View file

@ -50,6 +50,7 @@ import (
"github.com/strukturag/nextcloud-spreed-signaling/api"
"github.com/strukturag/nextcloud-spreed-signaling/async"
"github.com/strukturag/nextcloud-spreed-signaling/container"
"github.com/strukturag/nextcloud-spreed-signaling/internal"
"github.com/strukturag/nextcloud-spreed-signaling/log"
)
@ -79,7 +80,7 @@ type BackendServer struct {
turnvalid time.Duration
turnservers []string
statsAllowedIps atomic.Pointer[AllowedIps]
statsAllowedIps atomic.Pointer[container.IPList]
invalidSecret []byte
buffers BufferPool
@ -110,7 +111,7 @@ func NewBackendServer(ctx context.Context, config *goconf.ConfigFile, hub *Hub,
}
statsAllowed, _ := config.GetString("stats", "allowed_ips")
statsAllowedIps, err := ParseAllowedIps(statsAllowed)
statsAllowedIps, err := container.ParseIPList(statsAllowed)
if err != nil {
return nil, err
}
@ -118,7 +119,7 @@ func NewBackendServer(ctx context.Context, config *goconf.ConfigFile, hub *Hub,
if !statsAllowedIps.Empty() {
logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed)
} else {
statsAllowedIps = DefaultAllowedIps()
statsAllowedIps = container.DefaultAllowedIPs()
logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps)
}
@ -152,11 +153,11 @@ func NewBackendServer(ctx context.Context, config *goconf.ConfigFile, hub *Hub,
func (b *BackendServer) Reload(config *goconf.ConfigFile) {
statsAllowed, _ := config.GetString("stats", "allowed_ips")
if statsAllowedIps, err := ParseAllowedIps(statsAllowed); err == nil {
if statsAllowedIps, err := container.ParseIPList(statsAllowed); err == nil {
if !statsAllowedIps.Empty() {
b.logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed)
} else {
statsAllowedIps = DefaultAllowedIps()
statsAllowedIps = container.DefaultAllowedIPs()
b.logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps)
}
b.statsAllowedIps.Store(statsAllowedIps)
@ -980,7 +981,7 @@ func (b *BackendServer) allowStatsAccess(r *http.Request) bool {
}
allowed := b.statsAllowedIps.Load()
return allowed != nil && allowed.Allowed(ip)
return allowed != nil && allowed.Contains(ip)
}
func (b *BackendServer) validateStatsRequest(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {

View file

@ -19,25 +19,26 @@
* 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 signaling
package container
import (
"bytes"
"fmt"
"net"
"slices"
"strings"
"github.com/strukturag/nextcloud-spreed-signaling/internal"
)
type AllowedIps struct {
allowed []*net.IPNet
type IPList struct {
ips []*net.IPNet
}
func (a *AllowedIps) String() string {
func (a *IPList) String() string {
var b bytes.Buffer
b.WriteString("[")
for idx, n := range a.allowed {
for idx, n := range a.ips {
if idx > 0 {
b.WriteString(", ")
}
@ -47,18 +48,14 @@ func (a *AllowedIps) String() string {
return b.String()
}
func (a *AllowedIps) Empty() bool {
return len(a.allowed) == 0
func (a *IPList) Empty() bool {
return len(a.ips) == 0
}
func (a *AllowedIps) Allowed(ip net.IP) bool {
for _, i := range a.allowed {
if i.Contains(ip) {
return true
}
}
return false
func (a *IPList) Contains(ip net.IP) bool {
return slices.ContainsFunc(a.ips, func(n *net.IPNet) bool {
return n.Contains(ip)
})
}
func parseIPNet(s string) (*net.IPNet, error) {
@ -83,7 +80,7 @@ func parseIPNet(s string) (*net.IPNet, error) {
return ipnet, nil
}
func ParseAllowedIps(allowed string) (*AllowedIps, error) {
func ParseIPList(allowed string) (*IPList, error) {
var allowedIps []*net.IPNet
for ip := range internal.SplitEntries(allowed, ",") {
i, err := parseIPNet(ip)
@ -93,13 +90,13 @@ func ParseAllowedIps(allowed string) (*AllowedIps, error) {
allowedIps = append(allowedIps, i)
}
result := &AllowedIps{
allowed: allowedIps,
result := &IPList{
ips: allowedIps,
}
return result, nil
}
func DefaultAllowedIps() *AllowedIps {
func DefaultAllowedIPs() *IPList {
allowedIps := []*net.IPNet{
{
IP: net.ParseIP("127.0.0.1"),
@ -111,8 +108,8 @@ func DefaultAllowedIps() *AllowedIps {
},
}
result := &AllowedIps{
allowed: allowedIps,
result := &IPList{
ips: allowedIps,
}
return result
}
@ -129,8 +126,8 @@ var (
}
)
func DefaultPrivateIps() *AllowedIps {
allowed, err := ParseAllowedIps(strings.Join(privateIpNets, ","))
func DefaultPrivateIPs() *IPList {
allowed, err := ParseIPList(strings.Join(privateIpNets, ","))
if err != nil {
panic(fmt.Errorf("could not parse private ips %+v: %w", privateIpNets, err))
}

View file

@ -19,7 +19,7 @@
* 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 signaling
package container
import (
"net"
@ -29,42 +29,67 @@ import (
"github.com/stretchr/testify/require"
)
func TestAllowedIps(t *testing.T) {
func TestIPList(t *testing.T) {
t.Parallel()
require := require.New(t)
a, err := ParseAllowedIps("127.0.0.1, 192.168.0.1, 192.168.1.1/24")
a, err := ParseIPList("127.0.0.1, 192.168.0.1, 192.168.1.1/24")
require.NoError(err)
require.False(a.Empty())
require.Equal(`[127.0.0.1/32, 192.168.0.1/32, 192.168.1.0/24]`, a.String())
allowed := []string{
contained := []string{
"127.0.0.1",
"192.168.0.1",
"192.168.1.1",
"192.168.1.100",
}
notAllowed := []string{
notContained := []string{
"192.168.0.2",
"10.1.2.3",
}
for _, addr := range allowed {
for _, addr := range contained {
t.Run(addr, func(t *testing.T) {
t.Parallel()
assert := assert.New(t)
if ip := net.ParseIP(addr); assert.NotNil(ip, "error parsing %s", addr) {
assert.True(a.Allowed(ip), "should allow %s", addr)
assert.True(a.Contains(ip), "should contain %s", addr)
}
})
}
for _, addr := range notAllowed {
for _, addr := range notContained {
t.Run(addr, func(t *testing.T) {
t.Parallel()
assert := assert.New(t)
if ip := net.ParseIP(addr); assert.NotNil(ip, "error parsing %s", addr) {
assert.False(a.Allowed(ip), "should not allow %s", addr)
assert.False(a.Contains(ip), "should not contain %s", addr)
}
})
}
}
func TestDefaultAllowedIPs(t *testing.T) {
t.Parallel()
assert := assert.New(t)
ips := DefaultAllowedIPs()
assert.True(ips.Contains(net.ParseIP("127.0.0.1")))
assert.False(ips.Contains(net.ParseIP("127.1.0.1")))
assert.True(ips.Contains(net.ParseIP("::1")))
assert.False(ips.Contains(net.ParseIP("1.1.1.1")))
}
func TestDefaultPrivateIPs(t *testing.T) {
t.Parallel()
assert := assert.New(t)
ips := DefaultPrivateIPs()
assert.True(ips.Contains(net.ParseIP("127.0.0.1")))
assert.True(ips.Contains(net.ParseIP("127.1.0.1")))
assert.True(ips.Contains(net.ParseIP("::1")))
assert.True(ips.Contains(net.ParseIP("10.1.2.3")))
assert.True(ips.Contains(net.ParseIP("172.16.17.18")))
assert.True(ips.Contains(net.ParseIP("192.168.10.20")))
assert.False(ips.Contains(net.ParseIP("1.1.1.1")))
}

27
hub.go
View file

@ -53,6 +53,7 @@ import (
"github.com/strukturag/nextcloud-spreed-signaling/api"
"github.com/strukturag/nextcloud-spreed-signaling/async"
"github.com/strukturag/nextcloud-spreed-signaling/container"
"github.com/strukturag/nextcloud-spreed-signaling/internal"
"github.com/strukturag/nextcloud-spreed-signaling/log"
)
@ -133,7 +134,7 @@ var (
// Allow time differences of up to one minute between server and proxy.
tokenLeeway = time.Minute
DefaultTrustedProxies = DefaultPrivateIps()
DefaultTrustedProxies = container.DefaultPrivateIPs()
)
func init() {
@ -204,7 +205,7 @@ type Hub struct {
backendTimeout time.Duration
backend *BackendClient
trustedProxies atomic.Pointer[AllowedIps]
trustedProxies atomic.Pointer[container.IPList]
geoip *GeoLookup
geoipOverrides atomic.Pointer[map[*net.IPNet]string]
geoipUpdating atomic.Bool
@ -218,8 +219,8 @@ type Hub struct {
skipFederationVerify bool
federationTimeout time.Duration
allowedCandidates atomic.Pointer[AllowedIps]
blockedCandidates atomic.Pointer[AllowedIps]
allowedCandidates atomic.Pointer[container.IPList]
blockedCandidates atomic.Pointer[container.IPList]
}
func NewHub(ctx context.Context, config *goconf.ConfigFile, events AsyncEvents, rpcServer *GrpcServer, rpcClients *GrpcClients, etcdClient *EtcdClient, r *mux.Router, version string) (*Hub, error) {
@ -284,7 +285,7 @@ func NewHub(ctx context.Context, config *goconf.ConfigFile, events AsyncEvents,
}
trustedProxies, _ := config.GetString("app", "trustedproxies")
trustedProxiesIps, err := ParseAllowedIps(trustedProxies)
trustedProxiesIps, err := container.ParseIPList(trustedProxies)
if err != nil {
return nil, err
}
@ -419,7 +420,7 @@ func NewHub(ctx context.Context, config *goconf.ConfigFile, events AsyncEvents,
federationTimeout: federationTimeout,
}
if value, _ := config.GetString("mcu", "allowedcandidates"); value != "" {
allowed, err := ParseAllowedIps(value)
allowed, err := container.ParseIPList(value)
if err != nil {
return nil, fmt.Errorf("invalid allowedcandidates: %w", err)
}
@ -430,7 +431,7 @@ func NewHub(ctx context.Context, config *goconf.ConfigFile, events AsyncEvents,
logger.Printf("No candidates allowlist")
}
if value, _ := config.GetString("mcu", "blockedcandidates"); value != "" {
blocked, err := ParseAllowedIps(value)
blocked, err := container.ParseIPList(value)
if err != nil {
return nil, fmt.Errorf("invalid blockedcandidates: %w", err)
}
@ -574,7 +575,7 @@ func (h *Hub) Stop() {
func (h *Hub) Reload(ctx context.Context, config *goconf.ConfigFile) {
trustedProxies, _ := config.GetString("app", "trustedproxies")
if trustedProxiesIps, err := ParseAllowedIps(trustedProxies); err == nil {
if trustedProxiesIps, err := container.ParseIPList(trustedProxies); err == nil {
if !trustedProxiesIps.Empty() {
h.logger.Printf("Trusted proxies: %s", trustedProxiesIps)
} else {
@ -594,7 +595,7 @@ func (h *Hub) Reload(ctx context.Context, config *goconf.ConfigFile) {
}
if value, _ := config.GetString("mcu", "allowedcandidates"); value != "" {
if allowed, err := ParseAllowedIps(value); err != nil {
if allowed, err := container.ParseIPList(value); err != nil {
h.logger.Printf("invalid allowedcandidates: %s", err)
} else {
h.logger.Printf("Candidates allowlist: %s", allowed)
@ -605,7 +606,7 @@ func (h *Hub) Reload(ctx context.Context, config *goconf.ConfigFile) {
h.allowedCandidates.Store(nil)
}
if value, _ := config.GetString("mcu", "blockedcandidates"); value != "" {
if blocked, err := ParseAllowedIps(value); err != nil {
if blocked, err := container.ParseIPList(value); err != nil {
h.logger.Printf("invalid blockedcandidates: %s", err)
} else {
h.logger.Printf("Candidates blocklist: %s", blocked)
@ -3055,7 +3056,7 @@ func (h *Hub) GetServerInfoDialout() (result []BackendServerInfoDialout) {
return
}
func GetRealUserIP(r *http.Request, trusted *AllowedIps) string {
func GetRealUserIP(r *http.Request, trusted *container.IPList) string {
addr := r.RemoteAddr
if host, _, err := net.SplitHostPort(addr); err == nil {
addr = host
@ -3067,7 +3068,7 @@ func GetRealUserIP(r *http.Request, trusted *AllowedIps) string {
}
// Don't check any headers if the server can be reached by untrusted clients directly.
if trusted == nil || !trusted.Allowed(ip) {
if trusted == nil || !trusted.Contains(ip) {
return addr
}
@ -3094,7 +3095,7 @@ func GetRealUserIP(r *http.Request, trusted *AllowedIps) string {
continue
}
if trusted.Allowed(ip) {
if trusted.Contains(ip) {
lastTrusted = hop
continue
}

View file

@ -54,6 +54,7 @@ import (
"github.com/strukturag/nextcloud-spreed-signaling/api"
"github.com/strukturag/nextcloud-spreed-signaling/async"
"github.com/strukturag/nextcloud-spreed-signaling/container"
"github.com/strukturag/nextcloud-spreed-signaling/internal"
"github.com/strukturag/nextcloud-spreed-signaling/log"
"github.com/strukturag/nextcloud-spreed-signaling/test"
@ -3294,7 +3295,7 @@ func TestGetRealUserIP(t *testing.T) {
}
for _, tc := range testcases {
trustedProxies, err := ParseAllowedIps(tc.trusted)
trustedProxies, err := container.ParseIPList(tc.trusted)
if !assert.NoError(t, err, "invalid trusted proxies in %+v", tc) {
continue
}

View file

@ -36,6 +36,7 @@ import (
"github.com/strukturag/nextcloud-spreed-signaling/api"
"github.com/strukturag/nextcloud-spreed-signaling/async"
"github.com/strukturag/nextcloud-spreed-signaling/container"
"github.com/strukturag/nextcloud-spreed-signaling/internal"
"github.com/strukturag/nextcloud-spreed-signaling/log"
)
@ -151,8 +152,8 @@ type clientInterface interface {
type mcuJanusSettings struct {
mcuCommonSettings
allowedCandidates atomic.Pointer[AllowedIps]
blockedCandidates atomic.Pointer[AllowedIps]
allowedCandidates atomic.Pointer[container.IPList]
blockedCandidates atomic.Pointer[container.IPList]
}
func newMcuJanusSettings(ctx context.Context, config *goconf.ConfigFile) (*mcuJanusSettings, error) {
@ -182,7 +183,7 @@ func (s *mcuJanusSettings) load(config *goconf.ConfigFile) error {
s.setTimeout(mcuTimeout)
if value, _ := config.GetString("mcu", "allowedcandidates"); value != "" {
allowed, err := ParseAllowedIps(value)
allowed, err := container.ParseIPList(value)
if err != nil {
return fmt.Errorf("invalid allowedcandidates: %w", err)
}
@ -194,7 +195,7 @@ func (s *mcuJanusSettings) load(config *goconf.ConfigFile) error {
s.allowedCandidates.Store(nil)
}
if value, _ := config.GetString("mcu", "blockedcandidates"); value != "" {
blocked, err := ParseAllowedIps(value)
blocked, err := container.ParseIPList(value)
if err != nil {
return fmt.Errorf("invalid blockedcandidates: %w", err)
}

View file

@ -52,6 +52,7 @@ import (
signaling "github.com/strukturag/nextcloud-spreed-signaling"
"github.com/strukturag/nextcloud-spreed-signaling/api"
"github.com/strukturag/nextcloud-spreed-signaling/async"
"github.com/strukturag/nextcloud-spreed-signaling/container"
"github.com/strukturag/nextcloud-spreed-signaling/log"
)
@ -129,8 +130,8 @@ type ProxyServer struct {
upgrader websocket.Upgrader
tokens ProxyTokens
statsAllowedIps atomic.Pointer[signaling.AllowedIps]
trustedProxies atomic.Pointer[signaling.AllowedIps]
statsAllowedIps atomic.Pointer[container.IPList]
trustedProxies atomic.Pointer[container.IPList]
sid atomic.Uint64
cookie *signaling.SessionIdCodec
@ -252,7 +253,7 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config *
}
statsAllowed, _ := config.GetString("stats", "allowed_ips")
statsAllowedIps, err := signaling.ParseAllowedIps(statsAllowed)
statsAllowedIps, err := container.ParseIPList(statsAllowed)
if err != nil {
return nil, err
}
@ -260,12 +261,12 @@ func NewProxyServer(ctx context.Context, r *mux.Router, version string, config *
if !statsAllowedIps.Empty() {
logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed)
} else {
statsAllowedIps = signaling.DefaultAllowedIps()
statsAllowedIps = container.DefaultAllowedIPs()
logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps)
}
trustedProxies, _ := config.GetString("app", "trustedproxies")
trustedProxiesIps, err := signaling.ParseAllowedIps(trustedProxies)
trustedProxiesIps, err := container.ParseIPList(trustedProxies)
if err != nil {
return nil, err
}
@ -617,11 +618,11 @@ func (s *ProxyServer) ScheduleShutdown() {
func (s *ProxyServer) Reload(config *goconf.ConfigFile) {
statsAllowed, _ := config.GetString("stats", "allowed_ips")
if statsAllowedIps, err := signaling.ParseAllowedIps(statsAllowed); err == nil {
if statsAllowedIps, err := container.ParseIPList(statsAllowed); err == nil {
if !statsAllowedIps.Empty() {
s.logger.Printf("Only allowing access to the stats endpoint from %s", statsAllowed)
} else {
statsAllowedIps = signaling.DefaultAllowedIps()
statsAllowedIps = container.DefaultAllowedIPs()
s.logger.Printf("No IPs configured for the stats endpoint, only allowing access from %s", statsAllowedIps)
}
s.statsAllowedIps.Store(statsAllowedIps)
@ -630,7 +631,7 @@ func (s *ProxyServer) Reload(config *goconf.ConfigFile) {
}
trustedProxies, _ := config.GetString("app", "trustedproxies")
if trustedProxiesIps, err := signaling.ParseAllowedIps(trustedProxies); err == nil {
if trustedProxiesIps, err := container.ParseIPList(trustedProxies); err == nil {
if !trustedProxiesIps.Empty() {
s.logger.Printf("Trusted proxies: %s", trustedProxiesIps)
} else {
@ -1655,7 +1656,7 @@ func (s *ProxyServer) allowStatsAccess(r *http.Request) bool {
}
allowed := s.statsAllowedIps.Load()
return allowed != nil && allowed.Allowed(ip)
return allowed != nil && allowed.Contains(ip)
}
func (s *ProxyServer) validateStatsRequest(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {