From a0f06e539c0520dc819141dd19b50c9aa36d18ac Mon Sep 17 00:00:00 2001 From: Jannis Mattheis Date: Fri, 28 Jul 2023 21:38:07 +0200 Subject: [PATCH] Add dns fetching for external ip --- cmd/serve.go | 6 +++ config/config.go | 59 +++----------------------- config/ip.go | 92 ++++++++++++++++++++++++++++++++++++++++ config/ipdns/dns.go | 77 +++++++++++++++++++++++++++++++++ config/ipdns/provider.go | 7 +++ config/ipdns/static.go | 12 ++++++ screego.config.example | 14 +++++- turn/server.go | 19 ++++++--- ws/event_join.go | 7 ++- ws/event_share.go | 7 ++- ws/room.go | 24 +++++------ 11 files changed, 249 insertions(+), 75 deletions(-) create mode 100644 config/ip.go create mode 100644 config/ipdns/dns.go create mode 100644 config/ipdns/provider.go create mode 100644 config/ipdns/static.go diff --git a/cmd/serve.go b/cmd/serve.go index ba133df..07c65ae 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 29bf1cf..bad81c2 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 0000000..11887df --- /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 0000000..a6a793b --- /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 0000000..f81ff52 --- /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 0000000..182663e --- /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 c81a3af..61f9b05 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 81861e2..bdb95ee 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 90bd003..20daeac 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 475465a..50b1c0d 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 3ca9ff0..0b41ea7 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