The main concept of this sketch is to augment the IP specified for TURN into a range of gradually more precise versions:
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.
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)
+ }
+ }
+}