Repository: screego/server (language: go)

Issue:
Allow configuring a domain name instead of external ip
We are allowed to indicate the local IP of machine (e.g. `192.168.XX.XX`) by default. However, I noticed that we are not able to specify the external IP with `0.0.0.0`. `0.0.0.0` means to connect every available interfaces including the loopback as we all know and it is quite useful when it comes to floating IPs. Many of cloud providers tend not to set a public IP statically, which means it can be changed one day. Or, server might be hidden behind a load balancer. It would be great if you could include such a feature, therefore.


Discussion / hints:
0.0.0.0 can be used when you want to listen on interfaces. This is already possible to define for the listen addresses:
```ini
# The address the http server will listen on.
SCREEGO_SERVER_ADDRESS=0.0.0.0:5050

# The address the TURN server will listen on.
SCREEGO_TURN_ADDRESS=0.0.0.0:3478
```
https://screego.net/#/config

The IP inside SCREEGO_EXTERNAL_IP will be transmitted to clients, thus it needs to be something specific that can be used outside the server that hosts screego. Technically it would be possible to define a domain there, but I don't think it's worth the effort to implement this.
I didn't know the concept deeply. Thank you for informing @jmattheis Yeah my second question was that. It may need to solve the domain first and may bring some technical difficulties as you mentioned. I would want to have such a user experience anyway because of the reasons I featured in the issue.
Changed this to a feature request.
Just adding a note here that an external address would be great for us as we want o run this in Kubernetes which has no easy mechanism for fetching an external IP
Also wanted to add in here that this feature would be extremely helpful. I don't have a static IP, so I would want to provide the config my dynamic DNS.

Fix patch:
diff --git a/.golangci.yml b/.golangci.yml
index fcf2e5f4..8929d9f5 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -2,7 +2,6 @@ linters:
   enable:
   - asciicheck
   - bodyclose
-  - depguard
   - dogsled
   - errcheck
   - exhaustive
diff --git a/cmd/serve.go b/cmd/serve.go
index ba133df2..07c65ae9 100644
--- a/cmd/serve.go
+++ b/cmd/serve.go
@@ -30,6 +30,12 @@ func serveCmd(version string) cli.Command {
 			if exit {
 				os.Exit(1)
 			}
+
+			if _, _, err := conf.TurnIPProvider.Get(); err != nil {
+				// error is already logged by .Get()
+				os.Exit(1)
+			}
+
 			users, err := auth.ReadPasswordsFile(conf.UsersFile, conf.Secret, conf.SessionTimeoutSeconds)
 			if err != nil {
 				log.Fatal().Str("file", conf.UsersFile).Err(err).Msg("While loading users file")
diff --git a/config/config.go b/config/config.go
index 29bf1cfe..bad81c23 100644
--- a/config/config.go
+++ b/config/config.go
@@ -4,7 +4,6 @@ import (
 	"crypto/rand"
 	"errors"
 	"fmt"
-	"net"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -14,6 +13,7 @@ import (
 	"github.com/joho/godotenv"
 	"github.com/kelseyhightower/envconfig"
 	"github.com/rs/zerolog"
+	"github.com/screego/server/config/ipdns"
 	"github.com/screego/server/config/mode"
 )
 
@@ -59,11 +59,10 @@ type Config struct {
 	UsersFile          string   `split_words:"true"`
 	Prometheus         bool     `split_words:"true"`
 
-	CheckOrigin  func(string) bool `ignored:"true" json:"-"`
-	TurnExternal bool              `ignored:"true"`
-	TurnIPV4     net.IP            `ignored:"true"`
-	TurnIPV6     net.IP            `ignored:"true"`
-	TurnPort     string            `ignored:"true"`
+	CheckOrigin    func(string) bool `ignored:"true" json:"-"`
+	TurnExternal   bool              `ignored:"true"`
+	TurnIPProvider ipdns.Provider    `ignored:"true"`
+	TurnPort       string            `ignored:"true"`
 
 	CloseRoomWhenOwnerLeaves bool `default:"true" split_words:"true"`
 }
@@ -187,7 +186,7 @@ func Get() (Config, []FutureLog) {
 			logs = append(logs, futureFatal("SCREEGO_EXTERNAL_IP and SCREEGO_TURN_EXTERNAL_IP must not be both set"))
 		}
 
-		config.TurnIPV4, config.TurnIPV6, errs = validateExternalIP(config.TurnExternalIP, "SCREEGO_TURN_EXTERNAL_IP")
+		config.TurnIPProvider, errs = parseIPProvider(config.TurnExternalIP, "SCREEGO_TURN_EXTERNAL_IP")
 		config.TurnPort = config.TurnExternalPort
 		config.TurnExternal = true
 		logs = append(logs, errs...)
@@ -195,7 +194,7 @@ func Get() (Config, []FutureLog) {
 			logs = append(logs, futureFatal("SCREEGO_TURN_EXTERNAL_SECRET must be set if external TURN server is used"))
 		}
 	} else if len(config.ExternalIP) > 0 {
-		config.TurnIPV4, config.TurnIPV6, errs = validateExternalIP(config.ExternalIP, "SCREEGO_EXTERNAL_IP")
+		config.TurnIPProvider, errs = parseIPProvider(config.ExternalIP, "SCREEGO_EXTERNAL_IP")
 		logs = append(logs, errs...)
 		split := strings.Split(config.TurnAddress, ":")
 		config.TurnPort = split[len(split)-1]
@@ -222,50 +221,6 @@ func Get() (Config, []FutureLog) {
 	return config, logs
 }
 
-func validateExternalIP(ips []string, config string) (net.IP, net.IP, []FutureLog) {
-	if len(ips) == 0 {
-		return nil, nil, nil
-	}
-
-	first := ips[0]
-
-	firstParsed := net.ParseIP(first)
-	if firstParsed == nil || first == "0.0.0.0" {
-		return nil, nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: %s", config, first))}
-	}
-	firstIsIP4 := firstParsed.To4() != nil
-
-	if len(ips) == 1 {
-		if firstIsIP4 {
-			return firstParsed, nil, nil
-		}
-		return nil, firstParsed, nil
-	}
-
-	second := ips[1]
-
-	secondParsed := net.ParseIP(second)
-	if secondParsed == nil || second == "0.0.0.0" {
-		return nil, nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: %s", config, second))}
-	}
-
-	secondIsIP4 := secondParsed.To4() != nil
-
-	if firstIsIP4 == secondIsIP4 {
-		return nil, nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: the ips must be of different type ipv4/ipv6", config))}
-	}
-
-	if len(ips) > 2 {
-		return nil, nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: too many ips supplied", config))}
-	}
-
-	if !firstIsIP4 {
-		return secondParsed, firstParsed, nil
-	}
-
-	return firstParsed, secondParsed, nil
-}
-
 func getExecutableOrWorkDir() (string, *FutureLog) {
 	dir, err := getExecutableDir()
 	// when using `go run main.go` the executable lives in th temp directory therefore the env.development
diff --git a/config/ip.go b/config/ip.go
new file mode 100644
index 00000000..11887dfe
--- /dev/null
+++ b/config/ip.go
@@ -0,0 +1,92 @@
+package config
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"strings"
+	"time"
+
+	"github.com/screego/server/config/ipdns"
+)
+
+func parseIPProvider(ips []string, config string) (ipdns.Provider, []FutureLog) {
+	if len(ips) == 0 {
+		panic("must have at least one ip")
+	}
+
+	first := ips[0]
+	if strings.HasPrefix(first, "dns:") {
+		if len(ips) > 1 {
+			return nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: when dns server is specified, only one value is allowed", config))}
+		}
+
+		return parseDNS(strings.TrimPrefix(first, "dns:")), nil
+	}
+
+	return parseStatic(ips, config)
+}
+
+func parseStatic(ips []string, config string) (*ipdns.Static, []FutureLog) {
+	var static ipdns.Static
+
+	firstV4, errs := applyIPTo(config, ips[0], &static)
+	if errs != nil {
+		return nil, errs
+	}
+
+	if len(ips) == 1 {
+		return &static, nil
+	}
+
+	secondV4, errs := applyIPTo(config, ips[1], &static)
+	if errs != nil {
+		return nil, errs
+	}
+
+	if firstV4 == secondV4 {
+		return nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: the ips must be of different type ipv4/ipv6", config))}
+	}
+
+	if len(ips) > 2 {
+		return nil, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: too many ips supplied", config))}
+	}
+
+	return &static, nil
+}
+
+func applyIPTo(config, ip string, static *ipdns.Static) (bool, []FutureLog) {
+	parsed := net.ParseIP(ip)
+	if parsed == nil || ip == "0.0.0.0" {
+		return false, []FutureLog{futureFatal(fmt.Sprintf("invalid %s: %s", config, ip))}
+	}
+
+	v4 := parsed.To4() != nil
+	if v4 {
+		static.V4 = parsed
+	} else {
+		static.V6 = parsed
+	}
+	return v4, nil
+}
+
+func parseDNS(dnsString string) *ipdns.DNS {
+	var dns ipdns.DNS
+
+	parts := strings.SplitN(dnsString, "@", 2)
+
+	dns.Domain = parts[0]
+	dns.DNS = "system"
+	if len(parts) == 2 {
+		dns.DNS = parts[1]
+		dns.Resolver = &net.Resolver{
+			PreferGo: true,
+			Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
+				d := net.Dialer{Timeout: 10 * time.Second}
+				return d.DialContext(ctx, network, parts[1])
+			},
+		}
+	}
+
+	return &dns
+}
diff --git a/config/ipdns/dns.go b/config/ipdns/dns.go
new file mode 100644
index 00000000..a6a793bd
--- /dev/null
+++ b/config/ipdns/dns.go
@@ -0,0 +1,77 @@
+package ipdns
+
+import (
+	"context"
+	"errors"
+	"net"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/rs/zerolog/log"
+)
+
+type DNS struct {
+	sync.Mutex
+
+	DNS      string
+	Resolver *net.Resolver
+	Domain   string
+
+	refetch time.Time
+	v4      net.IP
+	v6      net.IP
+	err     error
+}
+
+func (s *DNS) Get() (net.IP, net.IP, error) {
+	s.Lock()
+	defer s.Unlock()
+
+	if s.refetch.Before(time.Now()) {
+		oldV4, oldV6 := s.v4, s.v6
+		s.v4, s.v6, s.err = s.lookup()
+		if s.err == nil {
+			if !oldV4.Equal(s.v4) || !oldV6.Equal(s.v6) {
+				log.Info().Str("v4", s.v4.String()).
+					Str("v6", s.v6.String()).
+					Str("domain", s.Domain).
+					Str("dns", s.DNS).
+					Msg("DNS External IP")
+			}
+			s.refetch = time.Now().Add(time.Minute)
+		} else {
+			// don't spam the dns server
+			s.refetch = time.Now().Add(time.Second)
+			log.Err(s.err).Str("domain", s.Domain).Str("dns", s.DNS).Msg("DNS External IP")
+		}
+	}
+
+	return s.v4, s.v6, s.err
+}
+
+func (s *DNS) lookup() (net.IP, net.IP, error) {
+	ips, err := s.Resolver.LookupIP(context.Background(), "ip", s.Domain)
+	if err != nil {
+		if dns, ok := err.(*net.DNSError); ok && s.DNS != "system" {
+			dns.Server = ""
+		}
+		return nil, nil, err
+	}
+
+	var v4, v6 net.IP
+	for _, ip := range ips {
+		isV6 := strings.Contains(ip.String(), ":")
+		if isV6 && v6 == nil {
+			v6 = ip
+		} else if !isV6 && v4 == nil {
+			v4 = ip
+		}
+	}
+
+	if v4 == nil && v6 == nil {
+		return nil, nil, errors.New("dns record doesn't have an A or AAAA record")
+	}
+
+	return v4, v6, nil
+}
diff --git a/config/ipdns/provider.go b/config/ipdns/provider.go
new file mode 100644
index 00000000..f81ff520
--- /dev/null
+++ b/config/ipdns/provider.go
@@ -0,0 +1,7 @@
+package ipdns
+
+import "net"
+
+type Provider interface {
+	Get() (net.IP, net.IP, error)
+}
diff --git a/config/ipdns/static.go b/config/ipdns/static.go
new file mode 100644
index 00000000..182663e9
--- /dev/null
+++ b/config/ipdns/static.go
@@ -0,0 +1,12 @@
+package ipdns
+
+import "net"
+
+type Static struct {
+	V4 net.IP
+	V6 net.IP
+}
+
+func (s *Static) Get() (net.IP, net.IP, error) {
+	return s.V4, s.V6, nil
+}
diff --git a/screego.config.example b/screego.config.example
index c81a3af9..61f9b05f 100644
--- a/screego.config.example
+++ b/screego.config.example
@@ -4,7 +4,12 @@
 # to find your external ip.
 #   curl 'https://api.ipify.org'
 # Example:
-#   192.168.178.2,2a01:c22:a87c:e500:2d8:61ff:fec7:f92a
+#   SCREEGO_EXTERNAL_IP=192.168.178.2,2a01:c22:a87c:e500:2d8:61ff:fec7:f92a
+#
+# If the server doesn't have a static ip, the ip can be obtained via a domain:
+#   SCREEGO_EXTERNAL_IP=dns:app.screego.net
+# You can also specify the dns server to use
+#   SCREEGO_EXTERNAL_IP=dns:app.screego.net@9.9.9.9
 SCREEGO_EXTERNAL_IP=
 
 # A secret which should be unique. Is used for cookie authentication.
@@ -47,7 +52,12 @@ SCREEGO_TURN_STRICT_AUTH=true
 # to find your external ip.
 #   curl 'https://api.ipify.org'
 # Example:
-#   192.168.178.2,2a01:c22:a87c:e500:2d8:61ff:fec7:f92a
+#   SCREEGO_TURN_EXTERNAL_IP=192.168.178.2,2a01:c22:a87c:e500:2d8:61ff:fec7:f92a
+#
+# If the turn server doesn't have a static ip, the ip can be obtained via a domain:
+#   SCREEGO_TURN_EXTERNAL_IP=dns:turn.screego.net
+# You can also specify the dns server to use
+#   SCREEGO_TURN_EXTERNAL_IP=dns:turn.screego.net@9.9.9.9
 SCREEGO_TURN_EXTERNAL_IP=
 
 # The port the external TURN server listens on.
diff --git a/turn/server.go b/turn/server.go
index 81861e2f..bdb95eef 100644
--- a/turn/server.go
+++ b/turn/server.go
@@ -12,6 +12,7 @@ import (
 	"github.com/pion/turn/v2"
 	"github.com/rs/zerolog/log"
 	"github.com/screego/server/config"
+	"github.com/screego/server/config/ipdns"
 	"github.com/screego/server/util"
 )
 
@@ -39,9 +40,8 @@ type Entry struct {
 const Realm = "screego"
 
 type Generator struct {
-	ipv4 net.IP
-	ipv6 net.IP
 	turn.RelayAddressGenerator
+	IPProvider ipdns.Provider
 }
 
 func (r *Generator) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) {
@@ -50,10 +50,16 @@ func (r *Generator) AllocatePacketConn(network string, requestedPort int) (net.P
 		return conn, addr, err
 	}
 	relayAddr := *addr.(*net.UDPAddr)
-	if r.ipv6 == nil || (relayAddr.IP.To4() != nil && r.ipv4 != nil) {
-		relayAddr.IP = r.ipv4
+
+	v4, v6, err := r.IPProvider.Get()
+	if err != nil {
+		return conn, addr, err
+	}
+
+	if v6 == nil || (relayAddr.IP.To4() != nil && v4 != nil) {
+		relayAddr.IP = v4
 	} else {
-		relayAddr.IP = r.ipv6
+		relayAddr.IP = v6
 	}
 	if err == nil {
 		log.Debug().Str("addr", addr.String()).Str("relayaddr", relayAddr.String()).Msg("TURN allocated")
@@ -92,9 +98,8 @@ func newInternalServer(conf config.Config) (Server, error) {
 	}
 
 	gen := &Generator{
-		ipv4:                  conf.TurnIPV4,
-		ipv6:                  conf.TurnIPV6,
 		RelayAddressGenerator: generator(conf),
+		IPProvider:            conf.TurnIPProvider,
 	}
 
 	_, err = turn.NewServer(turn.ServerConfig{
diff --git a/ws/event_join.go b/ws/event_join.go
index 90bd0038..20daeac4 100644
--- a/ws/event_join.go
+++ b/ws/event_join.go
@@ -44,11 +44,16 @@ func (e *Join) Execute(rooms *Rooms, current ClientInfo) error {
 	room.notifyInfoChanged()
 	usersJoinedTotal.Inc()
 
+	v4, v6, err := rooms.config.TurnIPProvider.Get()
+	if err != nil {
+		return err
+	}
+
 	for _, user := range room.Users {
 		if current.ID == user.ID || !user.Streaming {
 			continue
 		}
-		room.newSession(user.ID, current.ID, rooms)
+		room.newSession(user.ID, current.ID, rooms, v4, v6)
 	}
 
 	return nil
diff --git a/ws/event_share.go b/ws/event_share.go
index 475465a0..50b1c0d6 100644
--- a/ws/event_share.go
+++ b/ws/event_share.go
@@ -24,11 +24,16 @@ func (e *StartShare) Execute(rooms *Rooms, current ClientInfo) error {
 
 	room.Users[current.ID].Streaming = true
 
+	v4, v6, err := rooms.config.TurnIPProvider.Get()
+	if err != nil {
+		return err
+	}
+
 	for _, user := range room.Users {
 		if current.ID == user.ID {
 			continue
 		}
-		room.newSession(current.ID, user.ID, rooms)
+		room.newSession(current.ID, user.ID, rooms, v4, v6)
 	}
 
 	room.notifyInfoChanged()
diff --git a/ws/room.go b/ws/room.go
index 3ca9ff02..0b41ea72 100644
--- a/ws/room.go
+++ b/ws/room.go
@@ -31,7 +31,7 @@ const (
 	CloseDone      = "Read End"
 )
 
-func (r *Room) newSession(host, client xid.ID, rooms *Rooms) {
+func (r *Room) newSession(host, client xid.ID, rooms *Rooms, v4, v6 net.IP) {
 	id := xid.New()
 	r.Sessions[id] = &RoomSession{
 		Host:   host,
@@ -44,18 +44,18 @@ func (r *Room) newSession(host, client xid.ID, rooms *Rooms) {
 	switch r.Mode {
 	case ConnectionLocal:
 	case ConnectionSTUN:
-		iceHost = []outgoing.ICEServer{{URLs: rooms.addresses("stun", false)}}
-		iceClient = []outgoing.ICEServer{{URLs: rooms.addresses("stun", false)}}
+		iceHost = []outgoing.ICEServer{{URLs: rooms.addresses("stun", v4, v6, false)}}
+		iceClient = []outgoing.ICEServer{{URLs: rooms.addresses("stun", v4, v6, false)}}
 	case ConnectionTURN:
 		hostName, hostPW := rooms.turnServer.Credentials(id.String()+"host", r.Users[host].Addr)
 		clientName, clientPW := rooms.turnServer.Credentials(id.String()+"client", r.Users[client].Addr)
 		iceHost = []outgoing.ICEServer{{
-			URLs:       rooms.addresses("turn", true),
+			URLs:       rooms.addresses("turn", v4, v6, true),
 			Credential: hostPW,
 			Username:   hostName,
 		}}
 		iceClient = []outgoing.ICEServer{{
-			URLs:       rooms.addresses("turn", true),
+			URLs:       rooms.addresses("turn", v4, v6, true),
 			Credential: clientPW,
 			Username:   clientName,
 		}}
@@ -64,17 +64,17 @@ func (r *Room) newSession(host, client xid.ID, rooms *Rooms) {
 	r.Users[client].Write <- outgoing.ClientSession{Peer: host, ID: id, ICEServers: iceClient}
 }
 
-func (r *Rooms) addresses(prefix string, tcp bool) (result []string) {
-	if r.config.TurnIPV4 != nil {
-		result = append(result, fmt.Sprintf("%s:%s:%s", prefix, r.config.TurnIPV4.String(), r.config.TurnPort))
+func (r *Rooms) addresses(prefix string, v4, v6 net.IP, tcp bool) (result []string) {
+	if v4 != nil {
+		result = append(result, fmt.Sprintf("%s:%s:%s", prefix, v4.String(), r.config.TurnPort))
 		if tcp {
-			result = append(result, fmt.Sprintf("%s:%s:%s?transport=tcp", prefix, r.config.TurnIPV4.String(), r.config.TurnPort))
+			result = append(result, fmt.Sprintf("%s:%s:%s?transport=tcp", prefix, v4.String(), r.config.TurnPort))
 		}
 	}
-	if r.config.TurnIPV6 != nil {
-		result = append(result, fmt.Sprintf("%s:[%s]:%s", prefix, r.config.TurnIPV6.String(), r.config.TurnPort))
+	if v6 != nil {
+		result = append(result, fmt.Sprintf("%s:[%s]:%s", prefix, v6.String(), r.config.TurnPort))
 		if tcp {
-			result = append(result, fmt.Sprintf("%s:[%s]:%s?transport=tcp", prefix, r.config.TurnIPV6.String(), r.config.TurnPort))
+			result = append(result, fmt.Sprintf("%s:[%s]:%s?transport=tcp", prefix, v6.String(), r.config.TurnPort))
 		}
 	}
 	return


Test patch: