diff --git a/README b/README
index d65abb6..7aea85c 100644
--- a/README
+++ b/README
@@ -15,85 +15,6 @@ This step is optional.
echo 'god:topsecret' > data/passwd
-## Set up an ICE server
-
-ICE is the NAT and firewall traversal protocol used by WebRTC. ICE uses
-a variety of techniques for establishing a flow in the presence of
-a firewall; the two most effective techniques, STUN and TURN, require help
-from an external server. Whether you need a helping server depends both
-on your firewalling setup and on the networks of your users; for
-production use, you should probably use your own TURN server.
-
-### No ICE server
-
-If Galène is not firewalled (high-numbered ports are accessible from the
-Internet) and none of your users are on a restrictive network, then you
-need no ICE servers. There is nothing to do, skip to *Set up a group*
-below.
-
-### STUN server
-
-If Galène might be behind a firewall (high-numbered ports might or might
-not be accessible from the Internet), but none of your clients are on
-a restrictive network, then a STUN server is enough. It is usually safe
-to use a third-party STUN server, although doing that might violate the
-privacy of your users. Your `data/ice-servers.json` file should look like
-this:
-
- [
- {
- "urls": [
- "stun:stun.example.org"
- ]
- }
- ]
-
-### TURN server
-
-In practice, some of your users will be on restrictive networks: many
-enterprise networks only allow outgoing TCP to ports 80 and 443;
-university networks tend to additionally allow outgoing traffic to port
-1194. For best performance, your TURN server should be located close to
-Galène and close to your users, so you will want to run your own. If you
-use *coturn*, your `/etc/turnserver.conf` could look like this:
-
- listening-port=443
- lt-cred-mech
- user=galene:secret
- realm=galene.example.org
- syslog
-
-Your `ice-servers.json` should look like this:
-
- [
- {
- "urls": [
- "turn:turn.example.org:443",
- "turn:turn.example.org:443?transport=tcp"
- ],
- "username": "galene",
- "credential": "secret"
- }
- ]
-
-If you prefer to use coturn's `use-auth-secret` option, then your
-`ice-servers.json` should look like this:
-
- [
- {
- "urls": [
- "turn:turn.example.com:443",
- "turn:turn.example.com:443?transport=tcp"
- ],
- "username": "galene",
- "credential": "secret",
- "credentialType": "hmac-sha1"
- }
- ]
-
-For redundancy, you may set up multiple TURN servers, and ICE will use the
-first one that works.
-
## Set up a group
A group is set up by creating a file `groups/name.json`.
@@ -115,10 +36,7 @@ A group with one operator and two users looks like this:
"op": [{"username": "jch", "password": "1234"}],
"presenter": [
{"username": "mom", "password": "0000"},
- {
- "username": "dad",
- "password": "Pójdźże, kiń tę chmurność w głąb flaszy!"
- }
+ {"username": "dad", "password": "1234"}
]
}
@@ -138,6 +56,27 @@ that the relay test has been successful. (The relay test will fail if you
didn't configure a TURN server; this is normal, and nothing to worry
about.)
+## Configure your server's firewall
+
+If your server has a global IPv4 address and there is no firewall, there
+is nothing to do.
+
+If your server has a global IPv4 address, then the firewall must, at
+a strict minimum, allow incoming traffic to TCP port 8443 (or whatever is
+configured with the `-http` command-line option) and TCP port 1194 (or
+whatever is configured with the `-turn` command-line option). For best
+performance, it should also allow UDP traffic to the TURN port and UDP
+traffic to ephemeral (high-numbered) ports.
+
+If your server only has a global IPv6 address, then you should probably
+disable the built-in TURN server (`-turn ""`) and configure an external
+TURN server; see "ICE Servers" below.
+
+If your server is behind NAT, then you should configure your NAT device to
+forward, at a minimum, ports 8443 and 1194. In addition, you should add
+the option `-turn 192.0.2.1:1194` to Galène's command line, where `192.0.2.1`
+is your NAT's external (global) IPv4 address.
+
## Deploy to your server
Set up a user *galene* on your server, then do:
@@ -298,4 +237,70 @@ user entry with a hashed password looks like this:
}
}
---- Juliusz Chroboczek
+# ICE Servers
+
+ICE is the NAT and firewall traversal protocol used by WebRTC. ICE can
+make use of two kinds of servers to help with NAT traversal: STUN servers,
+that simply help punching holes in NATs, and TURN servers, that serve as
+relays for traffic. TURN is a superset of NAT: no STUN server is
+necessary if a TURN server is available.
+
+Galène includes a simple IPv4-only TURN server, which is controlled by the
+`-turn` command-line option. If the value of this option is the empty
+string `""`, then the built-in server is disabled. If the value of this
+option is a colon followed with a port number `:1194`, then the TURN
+server will listen on all public IPv4 addresses of the local host, over
+UDP and TCP. If the value of this option is a socket address, such as
+`192.0.2.1:1194`, then the TURN server will listen on all addresses of the
+local host but assume that the address seen by the clients is the one
+given in the option; this is the recommended configuration when running
+behind NAT with port forwarding.
+
+Some users may prefer to disable Galène's built in TURN server (`-turn ""`)
+and configure an external ICE server. In that case, the ICE configuration
+should appear in the file `data/ice-servers.json`. In the case of a STUN
+server, it should look like this:
+
+ [
+ {
+ "urls": [
+ "stun:stun.example.org"
+ ]
+ }
+ ]
+
+In the case of s single TURN server, the `ice-servers.json` file should
+look like this:
+
+ [
+ {
+ "urls": [
+ "turn:turn.example.org:443",
+ "turn:turn.example.org:443?transport=tcp"
+ ],
+ "username": "galene",
+ "credential": "secret"
+ }
+ ]
+
+If you prefer to use coturn's `use-auth-secret` option, then your
+`ice-servers.json` should look like this:
+
+ [
+ {
+ "Urls": [
+ "turn:turn.example.com:443",
+ "turn:turn.example.com:443?transport=tcp"
+ ],
+ "username": "galene",
+ "credential": "secret",
+ "credentialType": "hmac-sha1"
+ }
+ ]
+
+For redundancy, you may set up multiple TURN servers, and ICE will use the
+first one that works. If an `ice-servers.json` file is present and
+Galène's built-in TURN server is enabled, then the external server will be
+used in preference to the built-in server.
+
+-- Juliusz Chroboczek
diff --git a/galene.go b/galene.go
index c4a1e12..6c26a06 100644
--- a/galene.go
+++ b/galene.go
@@ -14,6 +14,7 @@ import (
"github.com/jech/galene/diskwriter"
"github.com/jech/galene/group"
"github.com/jech/galene/ice"
+ "github.com/jech/galene/turnserver"
"github.com/jech/galene/webserver"
)
@@ -42,6 +43,8 @@ func main() {
flag.BoolVar(&group.UseMDNS, "mdns", false, "gather mDNS addresses")
flag.BoolVar(&ice.ICERelayOnly, "relay-only", false,
"require use of TURN relays for all media traffic")
+ flag.StringVar(&turnserver.Address, "turn", ":1194",
+ "built-in TURN server address (\"\" to disable)")
flag.Parse()
if cpuprofile != "" {
@@ -86,6 +89,12 @@ func main() {
go group.ReadPublicGroups()
+ err := turnserver.Start()
+ if err != nil {
+ log.Printf("TURN: %v", err)
+ }
+ defer turnserver.Stop()
+
serverDone := make(chan struct{})
go func() {
err := webserver.Serve(httpAddr, dataDir)
diff --git a/go.mod b/go.mod
index 122d5de..e6440e4 100644
--- a/go.mod
+++ b/go.mod
@@ -9,6 +9,7 @@ require (
github.com/pion/rtcp v1.2.6
github.com/pion/rtp v1.6.2
github.com/pion/sdp/v3 v3.0.4
+ github.com/pion/turn/v2 v2.0.5
github.com/pion/webrtc/v3 v3.0.3
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
)
diff --git a/ice/ice.go b/ice/ice.go
index b926e93..e11e23f 100644
--- a/ice/ice.go
+++ b/ice/ice.go
@@ -14,6 +14,8 @@ import (
"time"
"github.com/pion/webrtc/v3"
+
+ "github.com/jech/galene/turnserver"
)
type Server struct {
@@ -101,6 +103,8 @@ func updateICEConfiguration() *configuration {
}
}
+ cf.ICEServers = append(cf.ICEServers, turnserver.ICEServers()...)
+
if ICERelayOnly {
cf.ICETransportPolicy = webrtc.ICETransportPolicyRelay
}
diff --git a/turnserver/turnserver.go b/turnserver/turnserver.go
new file mode 100644
index 0000000..44a8bbc
--- /dev/null
+++ b/turnserver/turnserver.go
@@ -0,0 +1,219 @@
+package turnserver
+
+import (
+ "crypto/rand"
+ "encoding/base64"
+ "errors"
+ "log"
+ "net"
+ "strconv"
+
+ "github.com/pion/turn/v2"
+ "github.com/pion/webrtc/v3"
+)
+
+var username string
+var password string
+var server *turn.Server
+var Address string
+var addresses []net.Addr
+
+func publicAddresses() ([]net.IP, error) {
+ addrs, err := net.InterfaceAddrs()
+ if err != nil {
+ return nil, err
+ }
+
+ var as []net.IP
+
+ for _, addr := range addrs {
+ switch addr := addr.(type) {
+ case *net.IPNet:
+ a := addr.IP.To4()
+ if a == nil {
+ continue
+ }
+ if !a.IsGlobalUnicast() {
+ continue
+ }
+ if a[0] == 10 ||
+ a[0] == 172 && a[1] >= 16 && a[1] < 32 ||
+ a[0] == 192 && a[1] == 168 {
+ continue
+ }
+ as = append(as, a)
+ }
+ }
+ return as, nil
+}
+
+func listener(a net.IP, port int, relay net.IP) (*turn.PacketConnConfig, *turn.ListenerConfig) {
+ var pcc *turn.PacketConnConfig
+ var lc *turn.ListenerConfig
+ s := net.JoinHostPort(a.String(), strconv.Itoa(port))
+
+ p, err := net.ListenPacket("udp4", s)
+ if err == nil {
+ pcc = &turn.PacketConnConfig{
+ PacketConn: p,
+ RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
+ RelayAddress: relay,
+ Address: a.String(),
+ },
+ }
+ } else {
+ log.Printf("TURN: listenPacket(%v): %v", s, err)
+ }
+
+ l, err := net.Listen("tcp4", s)
+ if err == nil {
+ lc = &turn.ListenerConfig{
+ Listener: l,
+ RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
+ RelayAddress: relay,
+ Address: a.String(),
+ },
+ }
+ } else {
+ log.Printf("TURN: listen(%v): %v", s, err)
+ }
+
+ return pcc, lc
+}
+
+func Start() error {
+ if server != nil {
+ return errors.New("TURN server already started")
+ }
+
+ if Address == "" {
+ return errors.New("built-in TURN server disabled")
+ }
+ addr, err := net.ResolveUDPAddr("udp4", Address)
+ if err != nil {
+ return err
+ }
+
+ username = "galene"
+ buf := make([]byte, 6)
+ _, err = rand.Read(buf)
+ if err != nil {
+ return err
+ }
+
+ buf2 := make([]byte, 8)
+ base64.RawStdEncoding.Encode(buf2, buf)
+ password = string(buf2)
+
+ var lcs []turn.ListenerConfig
+ var pccs []turn.PacketConnConfig
+
+ if addr.IP != nil && !addr.IP.IsUnspecified() {
+ a := addr.IP.To4()
+ if a == nil {
+ return errors.New("couldn't parse address")
+ }
+ pcc, lc := listener(net.IP{0, 0, 0, 0}, addr.Port, a)
+ if pcc != nil {
+ pccs = append(pccs, *pcc)
+ addresses = append(addresses, &net.UDPAddr{
+ IP: a,
+ Port: addr.Port,
+ })
+ }
+ if lc != nil {
+ lcs = append(lcs, *lc)
+ addresses = append(addresses, &net.TCPAddr{
+ IP: a,
+ Port: addr.Port,
+ })
+ }
+ } else {
+ as, err := publicAddresses()
+ if err != nil {
+ return err
+ }
+
+ if len(as) == 0 {
+ return errors.New("no public addresses")
+ }
+
+ for _, a := range as {
+ pcc, lc := listener(a, addr.Port, a)
+ if pcc != nil {
+ pccs = append(pccs, *pcc)
+ addresses = append(addresses, &net.UDPAddr{
+ IP: a,
+ Port: addr.Port,
+ })
+ }
+ if lc != nil {
+ lcs = append(lcs, *lc)
+ addresses = append(addresses, &net.TCPAddr{
+ IP: a,
+ Port: addr.Port,
+ })
+ }
+ }
+ }
+
+ if len(pccs) == 0 && len(lcs) == 0 {
+ return errors.New("couldn't establish any listeners")
+ }
+
+ server, err = turn.NewServer(turn.ServerConfig{
+ Realm: "galene.org",
+ AuthHandler: func(u, r string, src net.Addr) ([]byte, bool) {
+ if u != username || r != "galene.org" {
+ return nil, false
+ }
+ return turn.GenerateAuthKey(u, r, password), true
+ },
+ ListenerConfigs: lcs,
+ PacketConnConfigs: pccs,
+ })
+
+ if err != nil {
+ addresses = nil
+ return err
+ }
+
+ return nil
+}
+
+func ICEServers() []webrtc.ICEServer {
+ if len(addresses) == 0 {
+ return nil
+ }
+
+ var urls []string
+ for _, a := range addresses {
+ switch a := a.(type) {
+ case *net.UDPAddr:
+ urls = append(urls, "turn:"+a.String())
+ case *net.TCPAddr:
+ urls = append(urls, "turn:"+a.String()+"?transport=tcp")
+ default:
+ log.Printf("unexpected TURN address %T", a)
+ }
+ }
+
+ return []webrtc.ICEServer{
+ {
+ URLs: urls,
+ Username: username,
+ Credential: password,
+ },
+ }
+
+}
+
+func Stop() {
+ addresses = nil
+ if server == nil {
+ return
+ }
+ server.Close()
+ server = nil
+ return
+}