Galène videoconferencing server discussion list archives
 help / color / mirror / Atom feed
From: Dirk-Willem van Gulik <dirkx@webweaving.org>
To: Juliusz Chroboczek <jch@irif.fr>
Cc: galene@lists.galene.org
Subject: [Galene] Re: Turn binding to the ANY Address - even when specified
Date: Thu, 16 Jan 2025 18:20:54 +0100	[thread overview]
Message-ID: <DDD9C2E3-30A4-4268-9AF6-AE4DF96434F0@webweaving.org> (raw)
In-Reply-To: <87o7065356.wl-jch@irif.fr>

[-- Attachment #1: Type: text/plain, Size: 12084 bytes --]

On 16 Jan 2025, at 13:55, Juliusz Chroboczek <jch@irif.fr> wrote:
> 
>> Ignore this - there is a whole general class ...
> 
> The wonders of debugging ICE issues ;-)  Been there, done that, given up.

Right - on https://github.com/dirkx/galene/tree/fix_bind_turn_ip is a sketch for the ICE T-Shirt. 

The main concept of this sketch is to augment the IP specified for TURN into a range of gradually more precise versions:

	auto
	:1234			simply set the port, still bind to 0.0.0.0
	1.2.3.4:1234		bind specifically to that

all the way to

	1.2.3.4:9999/4.3.2.1:1234	

Where 1.2.3.4 is the external/visible/relay address (on port 9999) and galene actually bind()s/listen()s to 4.3.2.1, port 1234.  With 1.2.3.4:9999/:1234	 (i.e no listen address) going to the 0.0.0.0/bind anything. :1234, :1234/:4321 do not - they go, as before to scan of the public/non RFC1918 addresses.

Does it make sense to spend a few hours to making this draft a bit more real & systematically tested ? Once thing I've not quite understanding yet is why the relay generator just needs the relay IP but not the relay port.

Dw.



commit dac7ae531fb1ed9ec15c484de562674f1e73822b
Author: Dirk-Willem van Gulik <dirkx@webweaving.org>
Date:   Thu Jan 16 18:00:36 2025 +0100

    First draft of exposed/internal address separation.

diff --git a/INSTALL b/INSTALL
index c9bccaa..5843f47 100644
--- a/INSTALL
+++ b/INSTALL
@@ -210,8 +210,18 @@ Galène includes an IPv4-only TURN server, which is controlled by the
     the one given in the option; this is useful when running behind NAT
     with port forwarding set up.
 
+  * If an IP address is in the form [<ip>][:<port>][/[<ip>][:<port>] then
+    the left side is taken as the external (i.e. exposed and coded into
+    a turn URI) pair. And the additional right hand side as the internal
+    IP/port pair that is mapped to those external addresses. So for
+    example a configuration of 203.0.113.1:1194/10.11.0.1:2194 is for
+    a TURN server that is exposed (e.g. redirected from a firewall)
+    as  203.0.113.1:1194 but is known in the DMZ or on the other side
+    of NAT as running on 10.11.0.1:2194.
+
   * the default value is `auto`, which behaves like `:1194` if there is no
-    `data/ice-servers.json` file, and like `""` otherwise.
+    `data/ice-servers.json` file, and like `""` otherwise. In this case
+    galene will bind to the ANY address. 
 
 If the server is not accessible from the Internet, e.g. because of NAT or
 because it is behind a restrictive firewall, then you should configure
diff --git a/turnserver/turnserver.go b/turnserver/turnserver.go
index 1dcebe0..9ab37fe 100644
--- a/turnserver/turnserver.go
+++ b/turnserver/turnserver.go
@@ -8,21 +8,101 @@ import (
 	"net"
 	"strconv"
 	"sync"
+        "fmt"
+        "strings"
 
 	"github.com/pion/turn/v2"
 	"github.com/pion/webrtc/v3"
 )
 
+const DEFAULT_PORT = 1194
+
 var username string
 var password string
 var Address string
 
+// Generalize a TCP/IP address & port pair. When the
+// port is '0' - an `off' is assumed.
+//
+type UDPTCPAddr struct {
+	IP   net.IP
+	Port int
+	Zone string // IPv6 scoped addressing zone
+}
+
+// General inside/outside address; where the two are 
+// the same in the cannonical simple case; but, for example
+// in a DMZ or when NAT-ting; the internal address is 
+// that what the turns server listens on (i.e bind()); whereas
+// any turn:// URI's and so on are constructed with the 
+// outside address. The term 'Paired' is taken from NAT.
+//
+type PairedAddr struct {
+        exposedAddr UDPTCPAddr // Or Relay Address
+        internalAddr UDPTCPAddr
+}
+
+func (addr PairedAddr) String() string {
+        return fmt.Sprintf("%v/%v", addr.exposedAddr, addr.internalAddr);
+}
+
+func ResolveUDPTCPAddr(address string) (*UDPTCPAddr, error) {
+	// We're doing a 'cheat' here; and rely on the UDP translator; as we know
+        // that UDP/TCP are indentical with regard to port/addr structure.
+        addr, err := net.ResolveUDPAddr("udp", address)
+	if err != nil {
+		return nil, err
+	}
+        // Complete the cheat by just copying out the address details; but not the network familly
+        r := UDPTCPAddr { addr.IP, addr.Port, addr.Zone }
+	return &r, nil
+}
+
+func NewUDPTCPAddr(Address string) (*UDPTCPAddr, error) {
+        if Address == "" || Address == "off" {
+		return &UDPTCPAddr{}, nil
+        }
+        ad := Address
+        if Address == "auto" {
+                ad = fmt.Sprintf(":%v", DEFAULT_PORT)
+        } else
+        if strings.Index(ad,":") == -1 {
+                ad = fmt.Sprintf("%v:%v", ad, DEFAULT_PORT)
+        }
+        addr, err := ResolveUDPTCPAddr(ad)
+        if err != nil {
+                return nil, err
+        }
+        return addr, nil
+}
+
+func NewPairedAddr(str string) (*PairedAddr, error) {
+        i := strings.Index(str,"/")
+        left :=str
+        if i > 1 {
+	     left = str[0:i]
+             str = str[i+1:]
+        }
+        exposedAddr, err := NewUDPTCPAddr(left)
+        if err != nil {
+                return nil, err
+        }
+        internalAddr, err := NewUDPTCPAddr(str)
+        if err != nil {
+                return nil, err
+        }
+
+        e := PairedAddr { *exposedAddr, *internalAddr }
+        return &e, nil
+}
+
 var server struct {
 	mu        sync.Mutex
 	addresses []net.Addr
 	server    *turn.Server
 }
 
+// Remove any RFC 1918 addreses from a list of addresses.
 func publicAddresses() ([]net.IP, error) {
 	addrs, err := net.InterfaceAddrs()
 	if err != nil {
@@ -57,34 +137,38 @@ func listener(a net.IP, port int, relay net.IP) (*turn.PacketConnConfig, *turn.L
 	var lc *turn.ListenerConfig
- 	s := net.JoinHostPort(a.String(), strconv.Itoa(port))
+       as := a.String()
+       if a == nil { as = "" }
+       s := net.JoinHostPort(as, strconv.Itoa(port))
 
-	var g turn.RelayAddressGenerator
+	var g turn.RelayAddressGenerator 
+	raddr := a.String()
 	if relay == nil || relay.IsUnspecified() {
 		g = &turn.RelayAddressGeneratorNone{
 			Address: a.String(),
 		}
 	} else {
+                raddr = relay.String()
 		g = &turn.RelayAddressGeneratorStatic{
 			RelayAddress: relay,
 			Address:      a.String(),
 		}
 	}
 
-	p, err := net.ListenPacket("udp4", s)
+	p, err := net.ListenPacket("udp", s)
 	if err == nil {
 		pcc = &turn.PacketConnConfig{
 			PacketConn:            p,
 			RelayAddressGenerator: g,
 		}
+		log.Printf("TURN: listener on udp:%v, visible address: %v",s,raddr)
 	} else {
 		log.Printf("TURN: listenPacket(%v): %v", s, err)
 	}
 
-	l, err := net.Listen("tcp4", s)
+	l, err := net.Listen("tcp", s)
 	if err == nil {
 		lc = &turn.ListenerConfig{
 			Listener:              l,
 			RelayAddressGenerator: g,
 		}
+		log.Printf("TURN: listener on tcp:%v, visible address: %v",s,raddr)
 	} else {
 		log.Printf("TURN: listen(%v): %v", s, err)
 	}
@@ -99,20 +183,13 @@ func Start() error {
 	if server.server != nil {
 		return nil
 	}
-
-	if Address == "" {
-		return errors.New("built-in TURN server disabled")
-	}
-
-	ad := Address
-	if Address == "auto" {
-		ad = ":1194"
+        addressPair, err := NewPairedAddr(Address)
+        if err != nil {
+		return errors.New(fmt.Sprintf("TURN: Address error: %v", err))
+        }
+	if addressPair.internalAddr.Port == 0 {
+		return errors.New("TURN: built-in TURN server disabled")
 	}
-	addr, err := net.ResolveUDPAddr("udp4", ad)
-	if err != nil {
-		return err
-	}
-
 	username = "galene"
 	buf := make([]byte, 6)
 	_, err = rand.Read(buf)
@@ -126,26 +203,29 @@ func Start() error {
 
 	var lcs []turn.ListenerConfig
 	var pccs []turn.PacketConnConfig
-
-	if addr.IP != nil && !addr.IP.IsUnspecified() {
-		a := addr.IP.To4()
+	if addressPair.exposedAddr.IP != nil && !addressPair.exposedAddr.IP.IsUnspecified() {
+		a := addressPair.exposedAddr.IP.To4()
 		if a == nil {
-			return errors.New("couldn't parse address")
+			return errors.New("couldn't parse address/not an IPv4 address")
 		}
-		pcc, lc := listener(net.IP{0, 0, 0, 0}, addr.Port, a)
+		pcc, lc := listener(addressPair.internalAddr.IP, addressPair.internalAddr.Port, addressPair.exposedAddr.IP)
 		if pcc != nil {
 			pccs = append(pccs, *pcc)
 			server.addresses = append(server.addresses, &net.UDPAddr{
-				IP:   a,
-				Port: addr.Port,
+				IP:   addressPair.exposedAddr.IP,
+				Port: addressPair.exposedAddr.Port,
 			})
+			log.Printf("TURN: External address udp:%v:%v", 
+				addressPair.exposedAddr.IP, addressPair.exposedAddr.Port)
 		}
 		if lc != nil {
 			lcs = append(lcs, *lc)
 			server.addresses = append(server.addresses, &net.TCPAddr{
-				IP:   a,
-				Port: addr.Port,
+				IP:   addressPair.exposedAddr.IP,
+				Port: addressPair.exposedAddr.Port,
 			})
+			log.Printf("TURN: External address tcp:%v:%v", 
+				addressPair.exposedAddr.IP, addressPair.exposedAddr.Port)
 		}
 	} else {
 		as, err := publicAddresses()
@@ -158,24 +238,26 @@ func Start() error {
 		}
 
 		for _, a := range as {
-			pcc, lc := listener(a, addr.Port, nil)
+			pcc, lc := listener(a, addressPair.internalAddr.Port, nil)
 			if pcc != nil {
 				pccs = append(pccs, *pcc)
 				server.addresses = append(server.addresses,
 					&net.UDPAddr{
 						IP:   a,
-						Port: addr.Port,
+						Port: addressPair.exposedAddr.Port,
 					},
 				)
+				log.Printf("TURN: external address udp:%v:%v", a, addressPair.exposedAddr.Port)
 			}
 			if lc != nil {
 				lcs = append(lcs, *lc)
 				server.addresses = append(server.addresses,
 					&net.TCPAddr{
 						IP:   a,
-						Port: addr.Port,
+						Port: addressPair.exposedAddr.Port,
 					},
 				)
+				log.Printf("TURN: external address tcp:%v:%v", a, addressPair.exposedAddr.Port)
 			}
 		}
 	}
@@ -183,8 +265,7 @@ func Start() error {
 	if len(pccs) == 0 && len(lcs) == 0 {
 		return errors.New("couldn't establish any listeners")
 	}
-
-	log.Printf("Starting built-in TURN server on %v", addr.String())
+	log.Printf("TURN: Starting built-in TURN server")
 
 	server.server, err = turn.NewServer(turn.ServerConfig{
 		Realm: "galene.org",
diff --git a/turnserver/turnserver_test.go b/turnserver/turnserver_test.go
new file mode 100644
index 0000000..efbb870
--- /dev/null
+++ b/turnserver/turnserver_test.go
@@ -0,0 +1,39 @@
+package turnserver
+
+import (
+	"testing"
+)
+
+func TestParseAddr(t *testing.T) {
+	a := []struct{ p, g string }{
+		{"", 					"{<nil> 0 }/{<nil> 0 }"},
+		{"off", 				"{<nil> 0 }/{<nil> 0 }"},
+		{"auto",				"{<nil> 1194 }/{<nil> 1194 }"},
+		{":1234", 				"{<nil> 1234 }/{<nil> 1234 }"},
+		{":1234/:4321", 			"{<nil> 1234 }/{<nil> 4321 }"},
+		{"10.11.0.1:1234", 			"{10.11.0.1 1234 }/{10.11.0.1 1234 }"},
+		{"10.11.0.1:1234/:4321", 		"{10.11.0.1 1234 }/{<nil> 4321 }"},
+		{"10.11.0.1:1234/1.2.3.4:4321", 	"{10.11.0.1 1234 }/{1.2.3.4 4321 }"},
+		{"always-1-2-3-4.webweaving.org", 	"{1.2.3.4 1194 }/{1.2.3.4 1194 }"},
+		{"always-1-2-3-4.webweaving.org:4321", 	"{1.2.3.4 4321 }/{1.2.3.4 4321 }"},
+		{"always-1-2-3-4.webweaving.org:4321/:1234", 	"{1.2.3.4 4321 }/{<nil> 1234 }"},
+		{"always-1-2-3-4.webweaving.org:4321/127.0.0.1:1234", 	"{1.2.3.4 4321 }/{127.0.0.1 1234 }"},
+		{"always-1-2-3-4.webweaving.org:4321/127.0.0.1", 	"{1.2.3.4 4321 }/{127.0.0.1 1194 }"},
+/* Only works on an pure IPv4 machine
+		{"localhost:1234/1.2.3.4:4321", 	"{127.0.0.1 1234 }/{1.2.3.4 4321 }"},
+		{"always-1-2-3-4.webweaving.org:4321/localhost", 	"{1.2.3.4 4321 }/{127.0.0.1 1194 }"},
+		{"always-1-2-3-4.webweaving.org:4321/localhost:1234", 	"{1.2.3.4 4321 }/{127.0.0.1 1234 }"},
+*/
+	}
+
+	for _, pg := range a {
+		g, err := NewPairedAddr(pg.p)
+		if err != nil {
+			t.Errorf("Error: '%v' (not expected)", err)
+		}
+		if g.String() != pg.g {
+			t.Errorf("'%v', got '%v', expected '%v'",
+				pg.p, g, pg.g)
+		}
+	}
+}


[-- Attachment #2: Type: text/html, Size: 29935 bytes --]

      reply	other threads:[~2025-01-16 17:24 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-01-15 11:30 [Galene] " Dirk-Willem van Gulik
2025-01-15 14:03 ` [Galene] " Juliusz Chroboczek
2025-01-15 15:56   ` Dirk-Willem van Gulik
2025-01-16 12:15     ` Dirk-Willem van Gulik
2025-01-16 12:55       ` Juliusz Chroboczek
2025-01-16 17:20         ` Dirk-Willem van Gulik [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

  List information: https://lists.galene.org/postorius/lists/galene.lists.galene.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=DDD9C2E3-30A4-4268-9AF6-AE4DF96434F0@webweaving.org \
    --to=dirkx@webweaving.org \
    --cc=galene@lists.galene.org \
    --cc=jch@irif.fr \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox