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)
+ }
+ }
+}