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 { if exit {
os.Exit(1) 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) users, err := auth.ReadPasswordsFile(conf.UsersFile, conf.Secret, conf.SessionTimeoutSeconds)
if err != nil { if err != nil {
log.Fatal().Str("file", conf.UsersFile).Err(err).Msg("While loading users file") log.Fatal().Str("file", conf.UsersFile).Err(err).Msg("While loading users file")

View File

@@ -4,7 +4,6 @@ import (
"crypto/rand" "crypto/rand"
"errors" "errors"
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -14,6 +13,7 @@ import (
"github.com/joho/godotenv" "github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig" "github.com/kelseyhightower/envconfig"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/screego/server/config/ipdns"
"github.com/screego/server/config/mode" "github.com/screego/server/config/mode"
) )
@@ -61,8 +61,7 @@ type Config struct {
CheckOrigin func(string) bool `ignored:"true" json:"-"` CheckOrigin func(string) bool `ignored:"true" json:"-"`
TurnExternal bool `ignored:"true"` TurnExternal bool `ignored:"true"`
TurnIPV4 net.IP `ignored:"true"` TurnIPProvider ipdns.Provider `ignored:"true"`
TurnIPV6 net.IP `ignored:"true"`
TurnPort string `ignored:"true"` TurnPort string `ignored:"true"`
CloseRoomWhenOwnerLeaves bool `default:"true" split_words:"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")) 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.TurnPort = config.TurnExternalPort
config.TurnExternal = true config.TurnExternal = true
logs = append(logs, errs...) 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")) logs = append(logs, futureFatal("SCREEGO_TURN_EXTERNAL_SECRET must be set if external TURN server is used"))
} }
} else if len(config.ExternalIP) > 0 { } 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...) logs = append(logs, errs...)
split := strings.Split(config.TurnAddress, ":") split := strings.Split(config.TurnAddress, ":")
config.TurnPort = split[len(split)-1] config.TurnPort = split[len(split)-1]
@@ -222,50 +221,6 @@ func Get() (Config, []FutureLog) {
return config, logs 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) { func getExecutableOrWorkDir() (string, *FutureLog) {
dir, err := getExecutableDir() dir, err := getExecutableDir()
// when using `go run main.go` the executable lives in th temp directory therefore the env.development // 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. # to find your external ip.
# curl 'https://api.ipify.org' # curl 'https://api.ipify.org'
# Example: # 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= SCREEGO_EXTERNAL_IP=
# A secret which should be unique. Is used for cookie authentication. # 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. # to find your external ip.
# curl 'https://api.ipify.org' # curl 'https://api.ipify.org'
# Example: # 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= SCREEGO_TURN_EXTERNAL_IP=
# The port the external TURN server listens on. # The port the external TURN server listens on.

View File

@@ -12,6 +12,7 @@ import (
"github.com/pion/turn/v2" "github.com/pion/turn/v2"
"github.com/rs/zerolog/log" "github.com/rs/zerolog/log"
"github.com/screego/server/config" "github.com/screego/server/config"
"github.com/screego/server/config/ipdns"
"github.com/screego/server/util" "github.com/screego/server/util"
) )
@@ -39,9 +40,8 @@ type Entry struct {
const Realm = "screego" const Realm = "screego"
type Generator struct { type Generator struct {
ipv4 net.IP
ipv6 net.IP
turn.RelayAddressGenerator turn.RelayAddressGenerator
IPProvider ipdns.Provider
} }
func (r *Generator) AllocatePacketConn(network string, requestedPort int) (net.PacketConn, net.Addr, error) { 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 return conn, addr, err
} }
relayAddr := *addr.(*net.UDPAddr) 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 { } else {
relayAddr.IP = r.ipv6 relayAddr.IP = v6
} }
if err == nil { if err == nil {
log.Debug().Str("addr", addr.String()).Str("relayaddr", relayAddr.String()).Msg("TURN allocated") 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{ gen := &Generator{
ipv4: conf.TurnIPV4,
ipv6: conf.TurnIPV6,
RelayAddressGenerator: generator(conf), RelayAddressGenerator: generator(conf),
IPProvider: conf.TurnIPProvider,
} }
_, err = turn.NewServer(turn.ServerConfig{ _, err = turn.NewServer(turn.ServerConfig{

View File

@@ -44,11 +44,16 @@ func (e *Join) Execute(rooms *Rooms, current ClientInfo) error {
room.notifyInfoChanged() room.notifyInfoChanged()
usersJoinedTotal.Inc() usersJoinedTotal.Inc()
v4, v6, err := rooms.config.TurnIPProvider.Get()
if err != nil {
return err
}
for _, user := range room.Users { for _, user := range room.Users {
if current.ID == user.ID || !user.Streaming { if current.ID == user.ID || !user.Streaming {
continue continue
} }
room.newSession(user.ID, current.ID, rooms) room.newSession(user.ID, current.ID, rooms, v4, v6)
} }
return nil return nil

View File

@@ -24,11 +24,16 @@ func (e *StartShare) Execute(rooms *Rooms, current ClientInfo) error {
room.Users[current.ID].Streaming = true room.Users[current.ID].Streaming = true
v4, v6, err := rooms.config.TurnIPProvider.Get()
if err != nil {
return err
}
for _, user := range room.Users { for _, user := range room.Users {
if current.ID == user.ID { if current.ID == user.ID {
continue continue
} }
room.newSession(current.ID, user.ID, rooms) room.newSession(current.ID, user.ID, rooms, v4, v6)
} }
room.notifyInfoChanged() room.notifyInfoChanged()

View File

@@ -31,7 +31,7 @@ const (
CloseDone = "Read End" 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() id := xid.New()
r.Sessions[id] = &RoomSession{ r.Sessions[id] = &RoomSession{
Host: host, Host: host,
@@ -44,18 +44,18 @@ func (r *Room) newSession(host, client xid.ID, rooms *Rooms) {
switch r.Mode { switch r.Mode {
case ConnectionLocal: case ConnectionLocal:
case ConnectionSTUN: case ConnectionSTUN:
iceHost = []outgoing.ICEServer{{URLs: rooms.addresses("stun", false)}} iceHost = []outgoing.ICEServer{{URLs: rooms.addresses("stun", v4, v6, false)}}
iceClient = []outgoing.ICEServer{{URLs: rooms.addresses("stun", false)}} iceClient = []outgoing.ICEServer{{URLs: rooms.addresses("stun", v4, v6, false)}}
case ConnectionTURN: case ConnectionTURN:
hostName, hostPW := rooms.turnServer.Credentials(id.String()+"host", r.Users[host].Addr) hostName, hostPW := rooms.turnServer.Credentials(id.String()+"host", r.Users[host].Addr)
clientName, clientPW := rooms.turnServer.Credentials(id.String()+"client", r.Users[client].Addr) clientName, clientPW := rooms.turnServer.Credentials(id.String()+"client", r.Users[client].Addr)
iceHost = []outgoing.ICEServer{{ iceHost = []outgoing.ICEServer{{
URLs: rooms.addresses("turn", true), URLs: rooms.addresses("turn", v4, v6, true),
Credential: hostPW, Credential: hostPW,
Username: hostName, Username: hostName,
}} }}
iceClient = []outgoing.ICEServer{{ iceClient = []outgoing.ICEServer{{
URLs: rooms.addresses("turn", true), URLs: rooms.addresses("turn", v4, v6, true),
Credential: clientPW, Credential: clientPW,
Username: clientName, 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} r.Users[client].Write <- outgoing.ClientSession{Peer: host, ID: id, ICEServers: iceClient}
} }
func (r *Rooms) addresses(prefix string, tcp bool) (result []string) { func (r *Rooms) addresses(prefix string, v4, v6 net.IP, tcp bool) (result []string) {
if r.config.TurnIPV4 != nil { if v4 != nil {
result = append(result, fmt.Sprintf("%s:%s:%s", prefix, r.config.TurnIPV4.String(), r.config.TurnPort)) result = append(result, fmt.Sprintf("%s:%s:%s", prefix, v4.String(), r.config.TurnPort))
if tcp { 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 { if v6 != nil {
result = append(result, fmt.Sprintf("%s:[%s]:%s", prefix, r.config.TurnIPV6.String(), r.config.TurnPort)) result = append(result, fmt.Sprintf("%s:[%s]:%s", prefix, v6.String(), r.config.TurnPort))
if tcp { 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 return