Add dns fetching for external ip

This commit is contained in:
Jannis Mattheis
2023-07-28 21:38:07 +02:00
parent 5251041fa7
commit a0f06e539c
11 changed files with 249 additions and 75 deletions

View File

@@ -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")

View File

@@ -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"
)
@@ -61,8 +61,7 @@ type Config struct {
CheckOrigin func(string) bool `ignored:"true" json:"-"`
TurnExternal bool `ignored:"true"`
TurnIPV4 net.IP `ignored:"true"`
TurnIPV6 net.IP `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

92
config/ip.go Normal file
View File

@@ -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
}

77
config/ipdns/dns.go Normal file
View File

@@ -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
}

7
config/ipdns/provider.go Normal file
View File

@@ -0,0 +1,7 @@
package ipdns
import "net"
type Provider interface {
Get() (net.IP, net.IP, error)
}

12
config/ipdns/static.go Normal file
View File

@@ -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
}

View File

@@ -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.

View File

@@ -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{

View File

@@ -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

View File

@@ -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()

View File

@@ -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