From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from weser.webweaving.org (weser.webweaving.org [148.251.234.232]) by mail.toke.dk (Postfix) with ESMTPS id 374B2AD56CC for ; Thu, 16 Jan 2025 18:24:48 +0100 (CET) Authentication-Results: mail.toke.dk; dkim=pass (1024-bit key; unprotected) header.d=webweaving.org header.i=@webweaving.org header.a=rsa-sha256 header.s=shared header.b=MVZGQWxP Received: from smtpclient.apple (83-85-39-103.cable.dynamic.v4.ziggo.nl [83.85.39.103]) (authenticated bits=0) by weser.webweaving.org (8.18.1/8.18.1) with ESMTPSA id 50GHKtpW023505 (version=TLSv1.2 cipher=ECDHE-ECDSA-AES256-GCM-SHA384 bits=256 verify=NO); Thu, 16 Jan 2025 18:20:56 +0100 (CET) (envelope-from dirkx@webweaving.org) DKIM-Signature: v=1; a=rsa-sha256; c=simple/simple; d=webweaving.org; s=shared; t=1737048057; bh=un9+VbYLjhD/FEBAn3RdlOZOAdNIbDeNbAh9f3id690=; h=From:Subject:Date:In-Reply-To:Cc:To:References; b=MVZGQWxPbQsSLJhwvp6TjwxrYWFUVYOPu6zuLZHYFqtB9xvxBLvs07dtOzoBFftDk pnxmon+l02KnbXsWWtEG6cfDPKLoMQPtZiinm21s7B9KxzkE4quWSeiU0N3JYIOaSG zWQXEW52OxctWc8u8DcwELtTohzwnRYNtsdluwSo= X-Authentication-Warning: weser.webweaving.org: Host 83-85-39-103.cable.dynamic.v4.ziggo.nl [83.85.39.103] claimed to be smtpclient.apple From: Dirk-Willem van Gulik Message-Id: Content-Type: multipart/alternative; boundary="Apple-Mail=_C8F83D8A-122A-4ED3-AD57-2DA4A0CAA578" Mime-Version: 1.0 (Mac OS X Mail 16.0 \(3826.300.87.4.3\)) Date: Thu, 16 Jan 2025 18:20:54 +0100 In-Reply-To: <87o7065356.wl-jch@irif.fr> To: Juliusz Chroboczek References: <03C86732-394C-4EF9-99A9-1643BB3AD6DB@webweaving.org> <87o708uqb4.wl-jch@irif.fr> <255B813A-A1F3-48A5-B4C8-4A340919BDCC@webweaving.org> <9942F645-605D-4F6D-AB5F-E7B5522CD3C3@webweaving.org> <87o7065356.wl-jch@irif.fr> X-Mailer: Apple Mail (2.3826.300.87.4.3) X-Greylist: Sender succeeded SMTP AUTH, not delayed by milter-greylist-4.6.4 (weser.webweaving.org [148.251.234.232]); Thu, 16 Jan 2025 18:20:57 +0100 (CET) X-MailFrom: dirkx@webweaving.org X-Mailman-Rule-Hits: max-size X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; news-moderation; no-subject; digests; suspicious-header Message-ID-Hash: IYEZWLZNDNISY2MYZUWFASZN4TNR6BXX X-Message-ID-Hash: IYEZWLZNDNISY2MYZUWFASZN4TNR6BXX X-Mailman-Approved-At: Thu, 16 Jan 2025 21:59:16 +0100 CC: galene@lists.galene.org X-Mailman-Version: 3.3.10 Precedence: list Subject: [Galene] Re: Turn binding to the ANY Address - even when specified List-Id: =?utf-8?q?Gal=C3=A8ne_videoconferencing_server_discussion_list?= Archived-At: List-Archive: List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: --Apple-Mail=_C8F83D8A-122A-4ED3-AD57-2DA4A0CAA578 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=utf-8 On 16 Jan 2025, at 13:55, Juliusz Chroboczek wrote: >=20 >> Ignore this - there is a whole general class ... >=20 > 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.=20 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=09 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 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=C3=A8ne 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. =20 + * If an IP address is in the form [][:][/[][:] = 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.=20 =20 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" =20 "github.com/pion/turn/v2" "github.com/pion/webrtc/v3" ) =20 +const DEFAULT_PORT =3D 1194 + var username string var password string var Address string =20 +// 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=20 +// the same in the cannonical simple case; but, for example +// in a DMZ or when NAT-ting; the internal address is=20 +// that what the turns server listens on (i.e bind()); whereas +// any turn:// URI's and so on are constructed with the=20 +// 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 :=3D net.ResolveUDPAddr("udp", address) + if err !=3D nil { + return nil, err + } + // Complete the cheat by just copying out the address details; = but not the network familly + r :=3D UDPTCPAddr { addr.IP, addr.Port, addr.Zone } + return &r, nil +} + +func NewUDPTCPAddr(Address string) (*UDPTCPAddr, error) { + if Address =3D=3D "" || Address =3D=3D "off" { + return &UDPTCPAddr{}, nil + } + ad :=3D Address + if Address =3D=3D "auto" { + ad =3D fmt.Sprintf(":%v", DEFAULT_PORT) + } else + if strings.Index(ad,":") =3D=3D -1 { + ad =3D fmt.Sprintf("%v:%v", ad, DEFAULT_PORT) + } + addr, err :=3D ResolveUDPTCPAddr(ad) + if err !=3D nil { + return nil, err + } + return addr, nil +} + +func NewPairedAddr(str string) (*PairedAddr, error) { + i :=3D strings.Index(str,"/") + left :=3Dstr + if i > 1 { + left =3D str[0:i] + str =3D str[i+1:] + } + exposedAddr, err :=3D NewUDPTCPAddr(left) + if err !=3D nil { + return nil, err + } + internalAddr, err :=3D NewUDPTCPAddr(str) + if err !=3D nil { + return nil, err + } + + e :=3D PairedAddr { *exposedAddr, *internalAddr } + return &e, nil +} + var server struct { mu sync.Mutex addresses []net.Addr server *turn.Server } =20 +// Remove any RFC 1918 addreses from a list of addresses. func publicAddresses() ([]net.IP, error) { addrs, err :=3D net.InterfaceAddrs() if err !=3D nil { @@ -57,34 +137,38 @@ func listener(a net.IP, port int, relay net.IP) = (*turn.PacketConnConfig, *turn.L var lc *turn.ListenerConfig - s :=3D net.JoinHostPort(a.String(), strconv.Itoa(port)) + as :=3D a.String() + if a =3D=3D nil { as =3D "" } + s :=3D net.JoinHostPort(as, strconv.Itoa(port)) =20 - var g turn.RelayAddressGenerator + var g turn.RelayAddressGenerator=20 + raddr :=3D a.String() if relay =3D=3D nil || relay.IsUnspecified() { g =3D &turn.RelayAddressGeneratorNone{ Address: a.String(), } } else { + raddr =3D relay.String() g =3D &turn.RelayAddressGeneratorStatic{ RelayAddress: relay, Address: a.String(), } } =20 - p, err :=3D net.ListenPacket("udp4", s) + p, err :=3D net.ListenPacket("udp", s) if err =3D=3D nil { pcc =3D &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) } =20 - l, err :=3D net.Listen("tcp4", s) + l, err :=3D net.Listen("tcp", s) if err =3D=3D nil { lc =3D &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 !=3D nil { return nil } - - if Address =3D=3D "" { - return errors.New("built-in TURN server disabled") - } - - ad :=3D Address - if Address =3D=3D "auto" { - ad =3D ":1194" + addressPair, err :=3D NewPairedAddr(Address) + if err !=3D nil { + return errors.New(fmt.Sprintf("TURN: Address error: %v", = err)) + } + if addressPair.internalAddr.Port =3D=3D 0 { + return errors.New("TURN: built-in TURN server disabled") } - addr, err :=3D net.ResolveUDPAddr("udp4", ad) - if err !=3D nil { - return err - } - username =3D "galene" buf :=3D make([]byte, 6) _, err =3D rand.Read(buf) @@ -126,26 +203,29 @@ func Start() error { =20 var lcs []turn.ListenerConfig var pccs []turn.PacketConnConfig - - if addr.IP !=3D nil && !addr.IP.IsUnspecified() { - a :=3D addr.IP.To4() + if addressPair.exposedAddr.IP !=3D nil && = !addressPair.exposedAddr.IP.IsUnspecified() { + a :=3D addressPair.exposedAddr.IP.To4() if a =3D=3D nil { - return errors.New("couldn't parse address") + return errors.New("couldn't parse address/not an = IPv4 address") } - pcc, lc :=3D listener(net.IP{0, 0, 0, 0}, addr.Port, a) + pcc, lc :=3D listener(addressPair.internalAddr.IP, = addressPair.internalAddr.Port, addressPair.exposedAddr.IP) if pcc !=3D nil { pccs =3D append(pccs, *pcc) server.addresses =3D 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",=20= + addressPair.exposedAddr.IP, = addressPair.exposedAddr.Port) } if lc !=3D nil { lcs =3D append(lcs, *lc) server.addresses =3D 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",=20= + addressPair.exposedAddr.IP, = addressPair.exposedAddr.Port) } } else { as, err :=3D publicAddresses() @@ -158,24 +238,26 @@ func Start() error { } =20 for _, a :=3D range as { - pcc, lc :=3D listener(a, addr.Port, nil) + pcc, lc :=3D listener(a, = addressPair.internalAddr.Port, nil) if pcc !=3D nil { pccs =3D append(pccs, *pcc) server.addresses =3D = 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 !=3D nil { lcs =3D append(lcs, *lc) server.addresses =3D = 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) =3D=3D 0 && len(lcs) =3D=3D 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") =20 server.server, err =3D 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 :=3D []struct{ p, g string }{ + {"", "{ 0 = }/{ 0 }"}, + {"off", "{ 0 = }/{ 0 }"}, + {"auto", "{ 1194 = }/{ 1194 }"}, + {":1234", "{ 1234 = }/{ 1234 }"}, + {":1234/:4321", "{ 1234 = }/{ 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 = }/{ 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 }/{ 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 :=3D range a { + g, err :=3D NewPairedAddr(pg.p) + if err !=3D nil { + t.Errorf("Error: '%v' (not expected)", err) + } + if g.String() !=3D pg.g { + t.Errorf("'%v', got '%v', expected '%v'", + pg.p, g, pg.g) + } + } +} --Apple-Mail=_C8F83D8A-122A-4ED3-AD57-2DA4A0CAA578 Content-Transfer-Encoding: quoted-printable Content-Type: text/html; charset=utf-8 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://git= hub.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=C3=A8ne 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 =3D 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 :=3D net.ResolveUDPAddr("udp", = address)
+ if err !=3D nil = {
+ = return nil, err
+ }
+     =    // Complete the cheat by just copying out the address = details; but not the network familly
+       =  r :=3D UDPTCPAddr { addr.IP, addr.Port, addr.Zone = }
+ = return &r, nil
+}
+
+func = NewUDPTCPAddr(Address string) (*UDPTCPAddr, error) {
+   =      if Address =3D=3D "" || Address =3D=3D "off" = {
+ = return &UDPTCPAddr{}, nil
+       =  }
+        ad :=3D = Address
+        if Address =3D=3D "auto" = {
+                ad = =3D fmt.Sprintf(":%v", DEFAULT_PORT)
+       =  } else
+        if = strings.Index(ad,":") =3D=3D -1 {
+       =          ad =3D fmt.Sprintf("%v:%v", ad, = DEFAULT_PORT)
+        }
+ =        addr, err :=3D = ResolveUDPTCPAddr(ad)
+        if err !=3D = nil {
+               =  return nil, err
+       =  }
+        return addr, = nil
+}
+
+func NewPairedAddr(str string) = (*PairedAddr, error) {
+        i :=3D = strings.Index(str,"/")
+        left = :=3Dstr
+        if i > 1 = {
+ =     left =3D str[0:i]
+       =       str =3D str[i+1:]
+       =  }
+        exposedAddr, err :=3D = NewUDPTCPAddr(left)
+        if err !=3D = nil {
+               =  return nil, err
+       =  }
+        internalAddr, err :=3D = NewUDPTCPAddr(str)
+        if err !=3D = nil {
+               =  return nil, err
+       =  }
+
+        e :=3D = 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 :=3D = net.InterfaceAddrs()
  if err !=3D nil {
@@ = -57,34 +137,38 @@ func listener(a net.IP, port int, relay net.IP) = (*turn.PacketConnConfig, *turn.L
  var lc = *turn.ListenerConfig
s :=3D = net.JoinHostPort(a.String(), strconv.Itoa(port))
+   =     as :=3D a.String()
+       if a = =3D=3D nil { as =3D "" }
+       s :=3D = net.JoinHostPort(as, = strconv.Itoa(port))
 
- var g = turn.RelayAddressGenerator
+ var g = turn.RelayAddressGenerator 
+ raddr :=3D = a.String()
  if relay =3D=3D nil || = relay.IsUnspecified() {
  g =3D = &turn.RelayAddressGeneratorNone{
  = Address: a.String(),
  }
  } else = {
+               =  raddr =3D relay.String()
  g = =3D &turn.RelayAddressGeneratorStatic{
  = RelayAddress: relay,
  Address:   =    a.String(),
  }
  = }
 
- p, err :=3D = net.ListenPacket("udp4", s)
+ p, err :=3D = net.ListenPacket("udp", s)
  if err =3D=3D nil = {
  pcc =3D = &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 :=3D net.Listen("tcp4", = s)
+ = l, err :=3D net.Listen("tcp", s)
  if err =3D=3D= nil {
  lc =3D = &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 !=3D nil = {
  return = nil
  }
-
- if = Address =3D=3D "" {
- return = errors.New("built-in TURN server disabled")
- = }
-
- ad :=3D Address
- if = Address =3D=3D "auto" {
- ad =3D = ":1194"
+        addressPair, err :=3D = NewPairedAddr(Address)
+        if err !=3D = nil {
+= return errors.New(fmt.Sprintf("TURN: Address = error: %v", err))
+       =  }
+ if addressPair.internalAddr.Port = =3D=3D 0 {
+ return errors.New("TURN: = built-in TURN server disabled")
  = }
- addr, err :=3D = net.ResolveUDPAddr("udp4", ad)
- if err !=3D nil = {
- = return err
- = }
-
  username =3D = "galene"
  buf :=3D make([]byte, = 6)
  _, err =3D = rand.Read(buf)
@@ -126,26 +203,29 @@ func Start() error = {
 
  var lcs = []turn.ListenerConfig
  var pccs = []turn.PacketConnConfig
-
- if = addr.IP !=3D nil && !addr.IP.IsUnspecified() {
- a = :=3D addr.IP.To4()
+ if addressPair.exposedAddr.IP !=3D = nil && !addressPair.exposedAddr.IP.IsUnspecified() = {
+ = a :=3D addressPair.exposedAddr.IP.To4()
  = if a =3D=3D nil {
- return = errors.New("couldn't parse address")
+ = return errors.New("couldn't parse address/not an IPv4 = address")
  }
- = pcc, lc :=3D listener(net.IP{0, 0, 0, 0}, addr.Port, = a)
+ = pcc, lc :=3D listener(addressPair.internalAddr.IP, = addressPair.internalAddr.Port, = addressPair.exposedAddr.IP)
  if pcc !=3D nil = {
  pccs =3D = append(pccs, *pcc)
  server.addresses = =3D 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 !=3D nil = {
  lcs =3D = append(lcs, *lc)
  server.addresses = =3D 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 :=3D publicAddresses()
@@ -158,24 +238,26 @@ = func Start() error {
  = }
 
  for _, a :=3D range as = {
- = pcc, lc :=3D listener(a, addr.Port, nil)
+ = pcc, lc :=3D listener(a, addressPair.internalAddr.Port, = nil)
  if pcc !=3D nil = {
  pccs =3D = append(pccs, *pcc)
  = server.addresses =3D = 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 !=3D nil = {
  lcs =3D = append(lcs, *lc)
  = server.addresses =3D = 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) =3D=3D 0 && = len(lcs) =3D=3D 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 =3D = 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 :=3D []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 :=3D range a = {
+ = g, err :=3D NewPairedAddr(pg.p)
+ = if err !=3D nil {
+ t.Errorf("Error: = '%v' (not expected)", err)
+ }
+ = if g.String() !=3D pg.g {
+ t.Errorf("'%v', = got '%v', expected '%v'",
+ pg.p, g, = pg.g)
+= }
+ = }
+}

= --Apple-Mail=_C8F83D8A-122A-4ED3-AD57-2DA4A0CAA578--