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