Pārlūkot izejas kodu

lib/connections: Add QUIC protocol support (fixes #5377) (#5737)

Audrius Butkevicius 6 gadi atpakaļ
vecāks
revīzija
e714df013f

+ 2 - 5
cmd/strelaypoolsrv/main.go

@@ -13,7 +13,6 @@ import (
 	"fmt"
 	"io/ioutil"
 	"log"
-	"math/rand"
 	"mime"
 	"net"
 	"net/http"
@@ -29,6 +28,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/prometheus/client_golang/prometheus/promhttp"
 	"github.com/syncthing/syncthing/cmd/strelaypoolsrv/auto"
+	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/relay/client"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/tlsutil"
@@ -370,10 +370,7 @@ func handleGetRequest(w http.ResponseWriter, r *http.Request) {
 	mut.RUnlock()
 
 	// Shuffle
-	for i := range relays {
-		j := rand.Intn(i + 1)
-		relays[i], relays[j] = relays[j], relays[i]
-	}
+	rand.Shuffle(relays)
 
 	json.NewEncoder(w).Encode(map[string][]*relay{
 		"relays": relays,

+ 4 - 3
go.mod

@@ -2,10 +2,12 @@ module github.com/syncthing/syncthing
 
 require (
 	github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362
+	github.com/AudriusButkevicius/pfilter v0.0.0-20190525131515-730b0de4d4de
 	github.com/AudriusButkevicius/recli v0.0.5
 	github.com/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e
 	github.com/calmh/du v1.0.1
 	github.com/calmh/xdr v1.1.0
+	github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d
 	github.com/chmduquesne/rollinghash v0.0.0-20180912150627-a60f8e7142b5
 	github.com/d4l3k/messagediff v1.2.1
 	github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568
@@ -17,10 +19,9 @@ require (
 	github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657
 	github.com/kr/pretty v0.1.0 // indirect
 	github.com/lib/pq v1.1.1
+	github.com/lucas-clemente/quic-go v0.11.1
 	github.com/mattn/go-isatty v0.0.7
 	github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338
-	github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3 // indirect
-	github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d // indirect
 	github.com/oschwald/geoip2-golang v1.3.0
 	github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 // indirect
 	github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59 // indirect
@@ -35,7 +36,7 @@ require (
 	github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
 	golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
 	golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
-	golang.org/x/text v0.0.0-20171227012246-e19ae1496984
+	golang.org/x/text v0.3.0
 	golang.org/x/time v0.0.0-20170927054726-6dc17368e09b
 	gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 // indirect
 	gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect

+ 34 - 26
go.sum

@@ -1,5 +1,7 @@
 github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362 h1:l4qGIzSY0WhdXdR74XMYAtfc0Ri/RJVM4p6x/E/+WkA=
 github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362/go.mod h1:CEaBhA5lh1spxbPOELh5wNLKGsVQoahjUhVrJViVK8s=
+github.com/AudriusButkevicius/pfilter v0.0.0-20190525131515-730b0de4d4de h1:w1VG0ehgPh2ucQGO7wL9TBmHLzMo4dduYwyp2lhs8+A=
+github.com/AudriusButkevicius/pfilter v0.0.0-20190525131515-730b0de4d4de/go.mod h1:1N0EEx/irz4B1qV17wW82TFbjQrE7oX316Cki6eDY0Q=
 github.com/AudriusButkevicius/recli v0.0.5 h1:xUa55PvWTHBm17T6RvjElRO3y5tALpdceH86vhzQ5wg=
 github.com/AudriusButkevicius/recli v0.0.5/go.mod h1:Q2E26yc6RvWWEz/TJ/goUp6yXvipYdJI096hpoaqsNs=
 github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
@@ -15,7 +17,11 @@ github.com/calmh/du v1.0.1 h1:uDCrDbXVVPrzxSNRkpj6nqSfwrl5uRWH3zvrJgl7RRo=
 github.com/calmh/du v1.0.1/go.mod h1:pHNccp4cXQeyDaiV3S7t5GN+eGOgynF0VSLxJjk9tLU=
 github.com/calmh/xdr v1.1.0 h1:U/Dd4CXNLoo8EiQ4ulJUXkgO1/EyQLgDKLgpY1SOoJE=
 github.com/calmh/xdr v1.1.0/go.mod h1:E8sz2ByAdXC8MbANf1LCRYzedSnnc+/sXXJs/PVqoeg=
+github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d h1:As4937T5NVbJ/DmZT9z33pyLEprMd6CUSfhbmMY57Io=
+github.com/ccding/go-stun v0.0.0-20180726100737-be486d185f3d/go.mod h1:3FK1bMar37f7jqVY7q/63k3OMX1c47pGCufzt3X0sYE=
 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/cheekybits/genny v1.0.0 h1:uGGa4nei+j20rOSeDeP5Of12XVm7TGUd4dJA9RDitfE=
+github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
 github.com/chmduquesne/rollinghash v0.0.0-20180912150627-a60f8e7142b5 h1:Wg96Dh0MLTanEaPO0OkGtUIaa2jOnShAIOVUIzRHUxo=
 github.com/chmduquesne/rollinghash v0.0.0-20180912150627-a60f8e7142b5/go.mod h1:Uc2I36RRfTAf7Dge82bi3RU0OQUmXT9iweIcPqvr8A0=
 github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
@@ -25,6 +31,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
 github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
 github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI=
 github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M=
+github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
@@ -32,18 +40,20 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me
 github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d h1:IngNQgbqr5ZOU0exk395Szrvkzes9Ilk1fmJfkw7d+M=
 github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
-github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
-github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
 github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4 h1:6o8aP0LGMKzo3NzwhhX6EJsiJ3ejmj+9yA/3p8Fjjlw=
 github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
 github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
 github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
 github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 github.com/golang/snappy v0.0.0-20170215233205-553a64147049 h1:K9KHZbXKpGydfDN0aZrsoHpLJlZsBrGMFWbgLDGnPZk=
 github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/jackpal/gateway v0.0.0-20161225004348-5795ac81146e h1:lS8IitpqG4RkZbEDlZg5Z7FvBdWLVjSVfsPGOKafEkI=
 github.com/jackpal/gateway v0.0.0-20161225004348-5795ac81146e/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
@@ -58,12 +68,12 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
-github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4=
 github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
-github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
-github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/lucas-clemente/quic-go v0.11.1 h1:zasajC848Dqq/+WqfqBCkmPw+YHNe1MBts/z7y7nXf4=
+github.com/lucas-clemente/quic-go v0.11.1/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
+github.com/marten-seemann/qtls v0.2.3 h1:0yWJ43C62LsZt08vuQJDK1uC1czUc3FJeCLPoNAI4vA=
+github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
 github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc=
 github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
 github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@@ -72,12 +82,11 @@ github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338 h1:USW1+zAUkUSvk
 github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
-github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3 h1:ZN7kHmC0iunA+4UPmERwsuMQan4lUnntO6WX6H1jOO8=
-github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d h1:r351oUAFgdsydkt/g+XR/iJWRwyxVpy6nkNdEl/QdAs=
-github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
-github.com/oschwald/geoip2-golang v1.1.0 h1:ACVPz5YqH4/jZkQdsp/PZc9shQVZmreCzAVNss5y3bo=
-github.com/oschwald/geoip2-golang v1.1.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
 github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8=
 github.com/oschwald/geoip2-golang v1.3.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE=
 github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 h1:XGLYUmodtNzThosQ8GkMvj9TiIB/uWsP8NfxKSa3aDc=
@@ -90,8 +99,6 @@ github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
-github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740=
-github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
 github.com/prometheus/client_golang v0.9.3 h1:9iH4JKXLzFbOAdtqv/a+j8aewx2Y8lAjAydhbaScPF8=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
 github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8=
@@ -99,13 +106,9 @@ github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE=
 github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
 github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
-github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 h1:PnBWHBf+6L0jOqq0gIVUe6Yk0/QMZ640k6NvkxcBf+8=
-github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
 github.com/prometheus/common v0.4.0 h1:7etb9YClo3a6HjLzfl6rIQaU+FDfi0VSX39io3aQ+DM=
 github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
-github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a h1:9a8MnZMP0X2nLJdBg+pBmGgkJlSaKC2KaQmTCk1XDtE=
-github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
 github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
 github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
@@ -128,29 +131,31 @@ github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw=
 github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
 github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 h1:okhMind4q9H1OxF44gNegWkiP4H/gsTFLalHFa4OOUI=
 github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0/go.mod h1:TTbGUfE+cXXceWtbTHq6lqcTvYPBKLNejBEbnUsQJtU=
-golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d h1:GrqEEc3+MtHKTsZrdIGVoYDgLpbSRzW1EF+nLu0PcHE=
-golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
 golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
 golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180926160741-c2ed4eda69e7/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06 h1:0oC8rFnE+74kEmuHZ46F6KHsMr5Gx2gUQPuNz28iQZM=
-golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/text v0.0.0-20171227012246-e19ae1496984 h1:ulYJn/BqO4fMRe1xAQzWjokgjsQLPpb21GltxXHI3fQ=
-golang.org/x/text v0.0.0-20171227012246-e19ae1496984/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e h1:ZytStCyV048ZqDsWHiYDdoI2Vd4msMcrDECFxS+tL9c=
+golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/time v0.0.0-20170927054726-6dc17368e09b h1:3X+R0qq1+64izd8es+EttB6qcY+JDlVmAhpRXl7gpzU=
 golang.org/x/time v0.0.0-20170927054726-6dc17368e09b/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -160,8 +165,11 @@ gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225/go.mod h1:cuepJuh7vyXfUy
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
 gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
 gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
-gopkg.in/yaml.v2 v2.0.0-20171116090243-287cf08546ab h1:yZ6iByf7GKeJ3gsd1Dr/xaj1DyJ//wxKX1Cdh8LhoAw=
-gopkg.in/yaml.v2 v2.0.0-20171116090243-287cf08546ab/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
 gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

+ 16 - 12
lib/api/mocked_config_test.go

@@ -74,51 +74,55 @@ func (c *mockedConfig) AddOrUpdatePendingDevice(device protocol.DeviceID, name,
 
 func (c *mockedConfig) AddOrUpdatePendingFolder(id, label string, device protocol.DeviceID) {}
 
-func (m *mockedConfig) MyName() string {
+func (c *mockedConfig) MyName() string {
 	return ""
 }
 
-func (m *mockedConfig) ConfigPath() string {
+func (c *mockedConfig) ConfigPath() string {
 	return ""
 }
 
-func (m *mockedConfig) SetGUI(gui config.GUIConfiguration) (config.Waiter, error) {
+func (c *mockedConfig) SetGUI(gui config.GUIConfiguration) (config.Waiter, error) {
 	return noopWaiter{}, nil
 }
 
-func (m *mockedConfig) SetOptions(opts config.OptionsConfiguration) (config.Waiter, error) {
+func (c *mockedConfig) SetOptions(opts config.OptionsConfiguration) (config.Waiter, error) {
 	return noopWaiter{}, nil
 }
 
-func (m *mockedConfig) Folder(id string) (config.FolderConfiguration, bool) {
+func (c *mockedConfig) Folder(id string) (config.FolderConfiguration, bool) {
 	return config.FolderConfiguration{}, false
 }
 
-func (m *mockedConfig) FolderList() []config.FolderConfiguration {
+func (c *mockedConfig) FolderList() []config.FolderConfiguration {
 	return nil
 }
 
-func (m *mockedConfig) SetFolder(fld config.FolderConfiguration) (config.Waiter, error) {
+func (c *mockedConfig) SetFolder(fld config.FolderConfiguration) (config.Waiter, error) {
 	return noopWaiter{}, nil
 }
 
-func (m *mockedConfig) Device(id protocol.DeviceID) (config.DeviceConfiguration, bool) {
+func (c *mockedConfig) Device(id protocol.DeviceID) (config.DeviceConfiguration, bool) {
 	return config.DeviceConfiguration{}, false
 }
 
-func (m *mockedConfig) RemoveDevice(id protocol.DeviceID) (config.Waiter, error) {
+func (c *mockedConfig) RemoveDevice(id protocol.DeviceID) (config.Waiter, error) {
 	return noopWaiter{}, nil
 }
 
-func (m *mockedConfig) IgnoredDevice(id protocol.DeviceID) bool {
+func (c *mockedConfig) IgnoredDevice(id protocol.DeviceID) bool {
 	return false
 }
 
-func (m *mockedConfig) IgnoredFolder(device protocol.DeviceID, folder string) bool {
+func (c *mockedConfig) IgnoredFolder(device protocol.DeviceID, folder string) bool {
 	return false
 }
 
-func (m *mockedConfig) GlobalDiscoveryServers() []string {
+func (c *mockedConfig) GlobalDiscoveryServers() []string {
+	return nil
+}
+
+func (c *mockedConfig) StunServers() []string {
 	return nil
 }
 

+ 29 - 2
lib/config/config.go

@@ -39,6 +39,8 @@ const (
 var (
 	// DefaultTCPPort defines default TCP port used if the URI does not specify one, for example tcp://0.0.0.0
 	DefaultTCPPort = 22000
+	// DefaultQUICPort defines default QUIC port used if the URI does not specify one, for example quic://0.0.0.0
+	DefaultQUICPort = 22000
 	// DefaultListenAddresses should be substituted when the configuration
 	// contains <listenAddress>default</listenAddress>. This is done by the
 	// "consumer" of the configuration as we don't want these saved to the
@@ -46,6 +48,7 @@ var (
 	DefaultListenAddresses = []string{
 		util.Address("tcp", net.JoinHostPort("0.0.0.0", strconv.Itoa(DefaultTCPPort))),
 		"dynamic+https://relays.syncthing.net/endpoint",
+		util.Address("quic", net.JoinHostPort("0.0.0.0", strconv.Itoa(DefaultQUICPort))),
 	}
 	DefaultGUIPort = 8384
 	// DefaultDiscoveryServersV4 should be substituted when the configuration
@@ -65,6 +68,30 @@ var (
 	DefaultDiscoveryServers = append(DefaultDiscoveryServersV4, DefaultDiscoveryServersV6...)
 	// DefaultTheme is the default and fallback theme for the web UI.
 	DefaultTheme = "default"
+	// Default stun servers should be substituted when the configuration
+	// contains <stunServer>default</stunServer>.
+
+	// DefaultPrimaryStunServers are servers provided by us (to avoid causing the public servers burden)
+	DefaultPrimaryStunServers = []string{
+		"stun.syncthing.net:3478",
+	}
+	DefaultSecondaryStunServers = []string{
+		"stun.callwithus.com:3478",
+		"stun.counterpath.com:3478",
+		"stun.counterpath.net:3478",
+		"stun.ekiga.net:3478",
+		"stun.ideasip.com:3478",
+		"stun.internetcalls.com:3478",
+		"stun.schlund.de:3478",
+		"stun.sipgate.net:10000",
+		"stun.sipgate.net:3478",
+		"stun.voip.aebc.com:3478",
+		"stun.voiparound.com:3478",
+		"stun.voipbuster.com:3478",
+		"stun.voipstunt.com:3478",
+		"stun.voxgratia.org:3478",
+		"stun.xten.com:3478",
+	}
 )
 
 func New(myID protocol.DeviceID) Configuration {
@@ -258,8 +285,8 @@ func (cfg *Configuration) clean() error {
 		existingFolders[folder.ID] = folder
 	}
 
-	cfg.Options.ListenAddresses = util.UniqueStrings(cfg.Options.ListenAddresses)
-	cfg.Options.GlobalAnnServers = util.UniqueStrings(cfg.Options.GlobalAnnServers)
+	cfg.Options.ListenAddresses = util.UniqueTrimmedStrings(cfg.Options.ListenAddresses)
+	cfg.Options.GlobalAnnServers = util.UniqueTrimmedStrings(cfg.Options.GlobalAnnServers)
 
 	if cfg.Version > 0 && cfg.Version < OldestHandledVersion {
 		l.Warnf("Configuration version %d is deprecated. Attempting best effort conversion, but please verify manually.", cfg.Version)

+ 8 - 2
lib/config/config_test.go

@@ -69,6 +69,9 @@ func TestDefaultValues(t *testing.T) {
 		UnackedNotificationIDs:  []string{},
 		DefaultFolderPath:       "~",
 		SetLowPriority:          true,
+		StunKeepaliveStartS:     180,
+		StunKeepaliveMinS:       20,
+		StunServers:             []string{"default"},
 	}
 
 	cfg := New(device1)
@@ -212,8 +215,11 @@ func TestOverriddenValues(t *testing.T) {
 			"channelNotification",   // added in 17->18 migration
 			"fsWatcherNotification", // added in 27->28 migration
 		},
-		DefaultFolderPath: "/media/syncthing",
-		SetLowPriority:    false,
+		DefaultFolderPath:   "/media/syncthing",
+		SetLowPriority:      false,
+		StunKeepaliveStartS: 9000,
+		StunKeepaliveMinS:   900,
+		StunServers:         []string{"foo"},
 	}
 
 	os.Unsetenv("STNOUPGRADE")

+ 23 - 16
lib/config/optionsconfiguration.go

@@ -52,6 +52,9 @@ type OptionsConfiguration struct {
 	DefaultFolderPath       string   `xml:"defaultFolderPath" json:"defaultFolderPath" default:"~"`
 	SetLowPriority          bool     `xml:"setLowPriority" json:"setLowPriority" default:"true"`
 	MaxConcurrentScans      int      `xml:"maxConcurrentScans" json:"maxConcurrentScans"`
+	StunKeepaliveStartS     int      `xml:"stunKeepaliveStartS" json:"stunKeepaliveStartS" default:"180"` // 0 for off
+	StunKeepaliveMinS       int      `xml:"stunKeepaliveMinS" json:"stunKeepaliveMinS" default:"20"`      // 0 for off
+	StunServers             []string `xml:"stunServer" json:"stunServers" default:"default"`
 
 	DeprecatedUPnPEnabled        bool     `xml:"upnpEnabled,omitempty" json:"-"`
 	DeprecatedUPnPLeaseM         int      `xml:"upnpLeaseMinutes,omitempty" json:"-"`
@@ -61,29 +64,33 @@ type OptionsConfiguration struct {
 	DeprecatedMinHomeDiskFreePct float64  `xml:"minHomeDiskFreePct,omitempty" json:"-"`
 }
 
-func (orig OptionsConfiguration) Copy() OptionsConfiguration {
-	c := orig
-	c.ListenAddresses = make([]string, len(orig.ListenAddresses))
-	copy(c.ListenAddresses, orig.ListenAddresses)
-	c.GlobalAnnServers = make([]string, len(orig.GlobalAnnServers))
-	copy(c.GlobalAnnServers, orig.GlobalAnnServers)
-	c.AlwaysLocalNets = make([]string, len(orig.AlwaysLocalNets))
-	copy(c.AlwaysLocalNets, orig.AlwaysLocalNets)
-	c.UnackedNotificationIDs = make([]string, len(orig.UnackedNotificationIDs))
-	copy(c.UnackedNotificationIDs, orig.UnackedNotificationIDs)
-	return c
+func (opts OptionsConfiguration) Copy() OptionsConfiguration {
+	optsCopy := opts
+	optsCopy.ListenAddresses = make([]string, len(opts.ListenAddresses))
+	copy(optsCopy.ListenAddresses, opts.ListenAddresses)
+	optsCopy.GlobalAnnServers = make([]string, len(opts.GlobalAnnServers))
+	copy(optsCopy.GlobalAnnServers, opts.GlobalAnnServers)
+	optsCopy.AlwaysLocalNets = make([]string, len(opts.AlwaysLocalNets))
+	copy(optsCopy.AlwaysLocalNets, opts.AlwaysLocalNets)
+	optsCopy.UnackedNotificationIDs = make([]string, len(opts.UnackedNotificationIDs))
+	copy(optsCopy.UnackedNotificationIDs, opts.UnackedNotificationIDs)
+	return optsCopy
 }
 
 // RequiresRestartOnly returns a copy with only the attributes that require
 // restart on change.
-func (orig OptionsConfiguration) RequiresRestartOnly() OptionsConfiguration {
-	copy := orig
+func (opts OptionsConfiguration) RequiresRestartOnly() OptionsConfiguration {
+	optsCopy := opts
 	blank := OptionsConfiguration{}
-	util.CopyMatchingTag(&blank, &copy, "restart", func(v string) bool {
+	util.CopyMatchingTag(&blank, &optsCopy, "restart", func(v string) bool {
 		if len(v) > 0 && v != "true" {
-			panic(fmt.Sprintf(`unexpected tag value: %s. expected untagged or "true"`, v))
+			panic(fmt.Sprintf(`unexpected tag value: %s. Expected untagged or "true"`, v))
 		}
 		return v != "true"
 	})
-	return copy
+	return optsCopy
+}
+
+func (opts OptionsConfiguration) IsStunDisabled() bool {
+	return opts.StunKeepaliveMinS < 1 || opts.StunKeepaliveStartS < 1 || !opts.NATEnabled
 }

+ 3 - 0
lib/config/testdata/overridenvalues.xml

@@ -36,5 +36,8 @@
         <tempIndexMinBlocks>100</tempIndexMinBlocks>
         <defaultFolderPath>/media/syncthing</defaultFolderPath>
         <setLowPriority>false</setLowPriority>
+        <stunKeepaliveStartS>9000</stunKeepaliveStartS>
+        <stunKeepaliveMinS>900</stunKeepaliveMinS>
+        <stunServer>foo</stunServer>
     </options>
 </configuration>

+ 28 - 2
lib/config/wrapper.go

@@ -14,6 +14,7 @@ import (
 	"github.com/syncthing/syncthing/lib/events"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/sync"
 	"github.com/syncthing/syncthing/lib/util"
 )
@@ -88,6 +89,7 @@ type Wrapper interface {
 
 	ListenAddresses() []string
 	GlobalDiscoveryServers() []string
+	StunServers() []string
 
 	Subscribe(c Committer)
 	Unsubscribe(c Committer)
@@ -105,6 +107,30 @@ type wrapper struct {
 	requiresRestart uint32 // an atomic bool
 }
 
+func (w *wrapper) StunServers() []string {
+	var addresses []string
+	for _, addr := range w.cfg.Options.StunServers {
+		switch addr {
+		case "default":
+			defaultPrimaryAddresses := make([]string, len(DefaultPrimaryStunServers))
+			copy(defaultPrimaryAddresses, DefaultPrimaryStunServers)
+			rand.Shuffle(defaultPrimaryAddresses)
+			addresses = append(addresses, defaultPrimaryAddresses...)
+
+			defaultSecondaryAddresses := make([]string, len(DefaultSecondaryStunServers))
+			copy(defaultSecondaryAddresses, DefaultSecondaryStunServers)
+			rand.Shuffle(defaultSecondaryAddresses)
+			addresses = append(addresses, defaultSecondaryAddresses...)
+		default:
+			addresses = append(addresses, addr)
+		}
+	}
+
+	addresses = util.UniqueTrimmedStrings(addresses)
+
+	return addresses
+}
+
 // Wrap wraps an existing Configuration structure and ties it to a file on
 // disk.
 func Wrap(path string, cfg Configuration) Wrapper {
@@ -442,7 +468,7 @@ func (w *wrapper) GlobalDiscoveryServers() []string {
 			servers = append(servers, srv)
 		}
 	}
-	return util.UniqueStrings(servers)
+	return util.UniqueTrimmedStrings(servers)
 }
 
 func (w *wrapper) ListenAddresses() []string {
@@ -455,7 +481,7 @@ func (w *wrapper) ListenAddresses() []string {
 			addresses = append(addresses, addr)
 		}
 	}
-	return util.UniqueStrings(addresses)
+	return util.UniqueTrimmedStrings(addresses)
 }
 
 func (w *wrapper) RequiresRestart() bool {

+ 0 - 12
lib/connections/config.go

@@ -1,12 +0,0 @@
-// Copyright (C) 2017 The Syncthing Authors.
-//
-// This Source Code Form is subject to the terms of the Mozilla Public
-// License, v. 2.0. If a copy of the MPL was not distributed with this file,
-// You can obtain one at http://mozilla.org/MPL/2.0/.
-
-package connections
-
-const (
-	tcpPriority   = 10
-	relayPriority = 200
-)

+ 128 - 0
lib/connections/quic_dial.go

@@ -0,0 +1,128 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+// +build go1.12
+
+package connections
+
+import (
+	"context"
+	"crypto/tls"
+	"net"
+	"net/url"
+	"time"
+
+	"github.com/lucas-clemente/quic-go"
+
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/connections/registry"
+	"github.com/syncthing/syncthing/lib/protocol"
+)
+
+const quicPriority = 100
+
+func init() {
+	factory := &quicDialerFactory{}
+	for _, scheme := range []string{"quic", "quic4", "quic6"} {
+		dialers[scheme] = factory
+	}
+}
+
+type quicDialer struct {
+	cfg    config.Wrapper
+	tlsCfg *tls.Config
+}
+
+func (d *quicDialer) Dial(id protocol.DeviceID, uri *url.URL) (internalConn, error) {
+	uri = fixupPort(uri, config.DefaultQUICPort)
+
+	addr, err := net.ResolveUDPAddr("udp", uri.Host)
+	if err != nil {
+		return internalConn{}, err
+	}
+
+	var conn net.PacketConn
+	closeConn := false
+	if listenConn := registry.Get(uri.Scheme, packetConnLess); listenConn != nil {
+		conn = listenConn.(net.PacketConn)
+	} else {
+		if packetConn, err := net.ListenPacket("udp", ":0"); err != nil {
+			return internalConn{}, err
+		} else {
+			closeConn = true
+			conn = packetConn
+		}
+	}
+
+	ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
+	session, err := quic.DialContext(ctx, conn, addr, uri.Host, d.tlsCfg, quicConfig)
+	if err != nil {
+		if closeConn {
+			_ = conn.Close()
+		}
+		return internalConn{}, err
+	}
+
+	// OpenStreamSync is blocks, but we want to make sure the connection is usable
+	// before we start killing off other connections, so do the dance.
+	ok := make(chan struct{})
+	go func() {
+		select {
+		case <-ok:
+			return
+		case <-time.After(10 * time.Second):
+			l.Debugln("timed out waiting for OpenStream on", session.RemoteAddr())
+			// This will unblock OpenStreamSync
+			_ = session.Close()
+		}
+	}()
+
+	stream, err := session.OpenStreamSync()
+	close(ok)
+	if err != nil {
+		// It's ok to close these, this does not close the underlying packetConn.
+		_ = session.Close()
+		if closeConn {
+			_ = conn.Close()
+		}
+		return internalConn{}, err
+	}
+
+	return internalConn{&quicTlsConn{session, stream}, connTypeQUICClient, quicPriority}, nil
+}
+
+func (d *quicDialer) RedialFrequency() time.Duration {
+	return time.Duration(d.cfg.Options().ReconnectIntervalS) * time.Second
+}
+
+type quicDialerFactory struct {
+	cfg    config.Wrapper
+	tlsCfg *tls.Config
+}
+
+func (quicDialerFactory) New(cfg config.Wrapper, tlsCfg *tls.Config) genericDialer {
+	return &quicDialer{
+		cfg:    cfg,
+		tlsCfg: tlsCfg,
+	}
+}
+
+func (quicDialerFactory) Priority() int {
+	return quicPriority
+}
+
+func (quicDialerFactory) AlwaysWAN() bool {
+	return false
+}
+
+func (quicDialerFactory) Valid(_ config.Configuration) error {
+	// Always valid
+	return nil
+}
+
+func (quicDialerFactory) String() string {
+	return "QUIC Dialer"
+}

+ 238 - 0
lib/connections/quic_listen.go

@@ -0,0 +1,238 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// +build go1.12
+
+package connections
+
+import (
+	"crypto/tls"
+	"net"
+	"net/url"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/lucas-clemente/quic-go"
+
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/connections/registry"
+	"github.com/syncthing/syncthing/lib/nat"
+	"github.com/syncthing/syncthing/lib/stun"
+)
+
+func init() {
+	factory := &quicListenerFactory{}
+	for _, scheme := range []string{"quic", "quic4", "quic6"} {
+		listeners[scheme] = factory
+	}
+}
+
+type quicListener struct {
+	nat atomic.Value
+
+	onAddressesChangedNotifier
+
+	uri     *url.URL
+	cfg     config.Wrapper
+	tlsCfg  *tls.Config
+	stop    chan struct{}
+	conns   chan internalConn
+	factory listenerFactory
+
+	address *url.URL
+	err     error
+	mut     sync.Mutex
+}
+
+func (t *quicListener) OnNATTypeChanged(natType stun.NATType) {
+	if natType != stun.NATUnknown {
+		l.Infof("%s detected NAT type: %s", t.uri, natType)
+	}
+	t.nat.Store(natType)
+}
+
+func (t *quicListener) OnExternalAddressChanged(address *stun.Host, via string) {
+	var uri *url.URL
+	if address != nil {
+		uri = &(*t.uri)
+		uri.Host = address.TransportAddr()
+	}
+
+	t.mut.Lock()
+	existingAddress := t.address
+	t.address = uri
+	t.mut.Unlock()
+
+	if uri != nil && (existingAddress == nil || existingAddress.String() != uri.String()) {
+		l.Infof("%s resolved external address %s (via %s)", t.uri, uri.String(), via)
+		t.notifyAddressesChanged(t)
+	} else if uri == nil && existingAddress != nil {
+		t.notifyAddressesChanged(t)
+	}
+}
+
+func (t *quicListener) Serve() {
+	t.mut.Lock()
+	t.err = nil
+	t.mut.Unlock()
+
+	network := strings.Replace(t.uri.Scheme, "quic", "udp", -1)
+
+	packetConn, err := net.ListenPacket(network, t.uri.Host)
+	if err != nil {
+		t.mut.Lock()
+		t.err = err
+		t.mut.Unlock()
+		l.Infoln("Listen (BEP/quic):", err)
+		return
+	}
+	defer func() { _ = packetConn.Close() }()
+
+	svc, conn := stun.New(t.cfg, t, packetConn)
+	defer func() { _ = conn.Close() }()
+
+	go svc.Serve()
+	defer svc.Stop()
+
+	registry.Register(t.uri.Scheme, conn)
+	defer registry.Unregister(t.uri.Scheme, conn)
+
+	listener, err := quic.Listen(conn, t.tlsCfg, quicConfig)
+	if err != nil {
+		t.mut.Lock()
+		t.err = err
+		t.mut.Unlock()
+		l.Infoln("Listen (BEP/quic):", err)
+		return
+	}
+
+	l.Infof("QUIC listener (%v) starting", packetConn.LocalAddr())
+	defer l.Infof("QUIC listener (%v) shutting down", packetConn.LocalAddr())
+
+	// Accept is forever, so handle stops externally.
+	go func() {
+		select {
+		case <-t.stop:
+			_ = listener.Close()
+		}
+	}()
+
+	for {
+		// Blocks forever, see https://github.com/lucas-clemente/quic-go/issues/1915
+		session, err := listener.Accept()
+
+		select {
+		case <-t.stop:
+			if err == nil {
+				_ = session.Close()
+			}
+			return
+		default:
+		}
+		if err != nil {
+			if err, ok := err.(net.Error); !ok || !err.Timeout() {
+				l.Warnln("Listen (BEP/quic): Accepting connection:", err)
+			}
+			continue
+		}
+
+		l.Debugln("connect from", session.RemoteAddr())
+
+		// Accept blocks forever, give it 10s to do it's thing.
+		ok := make(chan struct{})
+		go func() {
+			select {
+			case <-ok:
+				return
+			case <-t.stop:
+				_ = session.Close()
+			case <-time.After(10 * time.Second):
+				l.Debugln("timed out waiting for AcceptStream on", session.RemoteAddr())
+				_ = session.Close()
+			}
+		}()
+
+		stream, err := session.AcceptStream()
+		close(ok)
+		if err != nil {
+			l.Debugln("failed to accept stream from", session.RemoteAddr(), err.Error())
+			_ = session.Close()
+			continue
+		}
+
+		t.conns <- internalConn{&quicTlsConn{session, stream}, connTypeQUICServer, quicPriority}
+	}
+}
+
+func (t *quicListener) Stop() {
+	close(t.stop)
+}
+
+func (t *quicListener) URI() *url.URL {
+	return t.uri
+}
+
+func (t *quicListener) WANAddresses() []*url.URL {
+	uris := t.LANAddresses()
+	t.mut.Lock()
+	if t.address != nil {
+		uris = append(uris, t.address)
+	}
+	t.mut.Unlock()
+	return uris
+}
+
+func (t *quicListener) LANAddresses() []*url.URL {
+	return []*url.URL{t.uri}
+}
+
+func (t *quicListener) Error() error {
+	t.mut.Lock()
+	err := t.err
+	t.mut.Unlock()
+	return err
+}
+
+func (t *quicListener) String() string {
+	return t.uri.String()
+}
+
+func (t *quicListener) Factory() listenerFactory {
+	return t.factory
+}
+
+func (t *quicListener) NATType() string {
+	v := t.nat.Load().(stun.NATType)
+	if v == stun.NATUnknown || v == stun.NATError {
+		return "unknown"
+	}
+	return v.String()
+}
+
+type quicListenerFactory struct{}
+
+func (f *quicListenerFactory) Valid(config.Configuration) error {
+	return nil
+}
+
+func (f *quicListenerFactory) New(uri *url.URL, cfg config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
+	l := &quicListener{
+		uri:     fixupPort(uri, config.DefaultQUICPort),
+		cfg:     cfg,
+		tlsCfg:  tlsCfg,
+		conns:   conns,
+		stop:    make(chan struct{}),
+		factory: f,
+	}
+	l.nat.Store(stun.NATUnknown)
+	return l
+}
+
+func (quicListenerFactory) Enabled(cfg config.Configuration) bool {
+	return true
+}

+ 57 - 0
lib/connections/quic_misc.go

@@ -0,0 +1,57 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// +build go1.12
+
+package connections
+
+import (
+	"net"
+
+	"github.com/lucas-clemente/quic-go"
+)
+
+var (
+	quicConfig = &quic.Config{
+		ConnectionIDLength: 4,
+		KeepAlive:          true,
+	}
+)
+
+type quicTlsConn struct {
+	quic.Session
+	quic.Stream
+}
+
+func (q *quicTlsConn) Close() error {
+	sterr := q.Stream.Close()
+	seerr := q.Session.Close()
+	if sterr != nil {
+		return sterr
+	}
+	return seerr
+}
+
+// Sort available packet connections by ip address, preferring unspecified local address.
+func packetConnLess(i interface{}, j interface{}) bool {
+	iIsUnspecified := false
+	jIsUnspecified := false
+	iLocalAddr := i.(net.PacketConn).LocalAddr()
+	jLocalAddr := j.(net.PacketConn).LocalAddr()
+
+	if host, _, err := net.SplitHostPort(iLocalAddr.String()); err == nil {
+		iIsUnspecified = host == "" || net.ParseIP(host).IsUnspecified()
+	}
+	if host, _, err := net.SplitHostPort(jLocalAddr.String()); err == nil {
+		jIsUnspecified = host == "" || net.ParseIP(host).IsUnspecified()
+	}
+
+	if jIsUnspecified == iIsUnspecified {
+		return len(iLocalAddr.Network()) < len(jLocalAddr.Network())
+	}
+
+	return iIsUnspecified
+}

+ 92 - 0
lib/connections/quic_misc_test.go

@@ -0,0 +1,92 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// +build go1.12
+
+package connections
+
+import (
+	"net"
+	"testing"
+	"time"
+)
+
+type mockPacketConn struct {
+	addr mockedAddr
+}
+
+func (mockPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
+	panic("implement me")
+}
+
+func (mockPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	panic("implement me")
+}
+
+func (mockPacketConn) Close() error {
+	panic("implement me")
+}
+
+func (c *mockPacketConn) LocalAddr() net.Addr {
+	return c.addr
+}
+
+func (mockPacketConn) SetDeadline(t time.Time) error {
+	panic("implement me")
+}
+
+func (mockPacketConn) SetReadDeadline(t time.Time) error {
+	panic("implement me")
+}
+
+func (mockPacketConn) SetWriteDeadline(t time.Time) error {
+	panic("implement me")
+}
+
+type mockedAddr struct {
+	network string
+	addr    string
+}
+
+func (a mockedAddr) Network() string {
+	return a.network
+}
+
+func (a mockedAddr) String() string {
+	return a.addr
+}
+
+func TestPacketConnLess(t *testing.T) {
+	cases := []struct {
+		netA  string
+		addrA string
+		netB  string
+		addrB string
+	}{
+		// B is assumed the winner.
+		{"tcp", "127.0.0.1:1234", "tcp", ":1235"},
+		{"tcp", "127.0.0.1:1234", "tcp", "0.0.0.0:1235"},
+		{"tcp4", "0.0.0.0:1234", "tcp", "0.0.0.0:1235"}, // tcp4 on the first one
+	}
+
+	for i, testCase := range cases {
+
+		conns := []*mockPacketConn{
+			{mockedAddr{testCase.netA, testCase.addrA}},
+			{mockedAddr{testCase.netB, testCase.addrB}},
+		}
+
+		if packetConnLess(conns[0], conns[1]) {
+			t.Error(i, "unexpected")
+		}
+		if !packetConnLess(conns[1], conns[0]) {
+			t.Error(i, "unexpected")
+		}
+		if packetConnLess(conns[0], conns[0]) || packetConnLess(conns[1], conns[1]) {
+			t.Error(i, "unexpected")
+		}
+	}
+}

+ 89 - 0
lib/connections/registry/registry.go

@@ -0,0 +1,89 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+// Registry tracks connections/addresses on which we are listening on, to allow us to pick a connection/address that
+// has a NAT port mapping. This also makes our outgoing port stable and same as incoming port which should allow
+// better probability of punching through.
+package registry
+
+import (
+	"sort"
+	"strings"
+
+	"github.com/syncthing/syncthing/lib/sync"
+)
+
+var (
+	Default = New()
+)
+
+type Registry struct {
+	mut       sync.Mutex
+	available map[string][]interface{}
+}
+
+func New() *Registry {
+	return &Registry{
+		mut:       sync.NewMutex(),
+		available: make(map[string][]interface{}),
+	}
+}
+
+func (r *Registry) Register(scheme string, item interface{}) {
+	r.mut.Lock()
+	defer r.mut.Unlock()
+
+	r.available[scheme] = append(r.available[scheme], item)
+}
+
+func (r *Registry) Unregister(scheme string, item interface{}) {
+	r.mut.Lock()
+	defer r.mut.Unlock()
+
+	candidates := r.available[scheme]
+	for i, existingItem := range candidates {
+		if existingItem == item {
+			copy(candidates[i:], candidates[i+1:])
+			candidates[len(candidates)-1] = nil
+			r.available[scheme] = candidates[:len(candidates)-1]
+			break
+		}
+	}
+}
+
+func (r *Registry) Get(scheme string, less func(i, j interface{}) bool) interface{} {
+	r.mut.Lock()
+	defer r.mut.Unlock()
+
+	candidates := make([]interface{}, 0)
+	for availableScheme, items := range r.available {
+		// quic:// should be considered ok for both quic4:// and quic6://
+		if strings.HasPrefix(scheme, availableScheme) {
+			candidates = append(candidates, items...)
+		}
+	}
+
+	if len(candidates) == 0 {
+		return nil
+	}
+
+	sort.Slice(candidates, func(i, j int) bool {
+		return less(candidates[i], candidates[j])
+	})
+	return candidates[0]
+}
+
+func Register(scheme string, item interface{}) {
+	Default.Register(scheme, item)
+}
+
+func Unregister(scheme string, item interface{}) {
+	Default.Unregister(scheme, item)
+}
+
+func Get(scheme string, less func(i, j interface{}) bool) interface{} {
+	return Default.Get(scheme, less)
+}

+ 71 - 0
lib/connections/registry/registry_test.go

@@ -0,0 +1,71 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package registry
+
+import (
+	"testing"
+)
+
+func TestRegistry(t *testing.T) {
+	r := New()
+
+	if res := r.Get("int", intLess); res != nil {
+		t.Error("unexpected")
+	}
+
+	r.Register("int", 1)
+	r.Register("int", 11)
+	r.Register("int4", 4)
+	r.Register("int4", 44)
+	r.Register("int6", 6)
+	r.Register("int6", 66)
+
+	if res := r.Get("int", intLess).(int); res != 1 {
+		t.Error("unexpected", res)
+	}
+
+	// int is prefix of int4, so returns 1
+	if res := r.Get("int4", intLess).(int); res != 1 {
+		t.Error("unexpected", res)
+	}
+
+	r.Unregister("int", 1)
+
+	// Check that falls through to 11
+	if res := r.Get("int", intLess).(int); res != 11 {
+		t.Error("unexpected", res)
+	}
+
+	// 6 is smaller than 11 available in int.
+	if res := r.Get("int6", intLess).(int); res != 6 {
+		t.Error("unexpected", res)
+	}
+
+	// Unregister 11, int should be impossible to find
+	r.Unregister("int", 11)
+	if res := r.Get("int", intLess); res != nil {
+		t.Error("unexpected")
+	}
+
+	// Unregister a second time does nothing.
+	r.Unregister("int", 1)
+
+	// Can have multiple of the same
+	r.Register("int", 1)
+	r.Register("int", 1)
+	r.Unregister("int", 1)
+
+	if res := r.Get("int4", intLess).(int); res != 1 {
+		t.Error("unexpected", res)
+	}
+}
+
+func intLess(i, j interface{}) bool {
+	iInt := i.(int)
+	jInt := j.(int)
+	return iInt < jInt
+}

+ 2 - 0
lib/connections/relay_dial.go

@@ -17,6 +17,8 @@ import (
 	"github.com/syncthing/syncthing/lib/relay/client"
 )
 
+const relayPriority = 200
+
 func init() {
 	dialers["relay"] = relayDialerFactory{}
 }

+ 3 - 3
lib/connections/service.go

@@ -375,7 +375,7 @@ func (s *service) connect() {
 				}
 			}
 
-			addrs = util.UniqueStrings(addrs)
+			addrs = util.UniqueTrimmedStrings(addrs)
 
 			l.Debugln("Reconnect loop for", deviceID, addrs)
 
@@ -642,7 +642,7 @@ func (s *service) AllAddresses() []string {
 		}
 	}
 	s.listenersMut.RUnlock()
-	return util.UniqueStrings(addrs)
+	return util.UniqueTrimmedStrings(addrs)
 }
 
 func (s *service) ExternalAddresses() []string {
@@ -654,7 +654,7 @@ func (s *service) ExternalAddresses() []string {
 		}
 	}
 	s.listenersMut.RUnlock()
-	return util.UniqueStrings(addrs)
+	return util.UniqueTrimmedStrings(addrs)
 }
 
 func (s *service) ListenerStatus() map[string]ListenerStatusEntry {

+ 21 - 3
lib/connections/structs.go

@@ -9,6 +9,7 @@ package connections
 import (
 	"crypto/tls"
 	"fmt"
+	"io"
 	"net"
 	"net/url"
 	"time"
@@ -43,10 +44,19 @@ func (c completeConn) Close(err error) {
 	c.internalConn.Close()
 }
 
+type tlsConn interface {
+	io.ReadWriteCloser
+	ConnectionState() tls.ConnectionState
+	RemoteAddr() net.Addr
+	SetDeadline(time.Time) error
+	SetWriteDeadline(time.Time) error
+	LocalAddr() net.Addr
+}
+
 // internalConn is the raw TLS connection plus some metadata on where it
 // came from (type, priority).
 type internalConn struct {
-	*tls.Conn
+	tlsConn
 	connType connType
 	priority int
 }
@@ -58,6 +68,8 @@ const (
 	connTypeRelayServer
 	connTypeTCPClient
 	connTypeTCPServer
+	connTypeQUICClient
+	connTypeQUICServer
 )
 
 func (t connType) String() string {
@@ -70,6 +82,10 @@ func (t connType) String() string {
 		return "tcp-client"
 	case connTypeTCPServer:
 		return "tcp-server"
+	case connTypeQUICClient:
+		return "quic-client"
+	case connTypeQUICServer:
+		return "quic-server"
 	default:
 		return "unknown-type"
 	}
@@ -81,6 +97,8 @@ func (t connType) Transport() string {
 		return "relay"
 	case connTypeTCPClient, connTypeTCPServer:
 		return "tcp"
+	case connTypeQUICClient, connTypeQUICServer:
+		return "quic"
 	default:
 		return "unknown"
 	}
@@ -90,8 +108,8 @@ func (c internalConn) Close() {
 	// *tls.Conn.Close() does more than it says on the tin. Specifically, it
 	// sends a TLS alert message, which might block forever if the
 	// connection is dead and we don't have a deadline set.
-	c.SetWriteDeadline(time.Now().Add(250 * time.Millisecond))
-	c.Conn.Close()
+	_ = c.SetWriteDeadline(time.Now().Add(250 * time.Millisecond))
+	_ = c.tlsConn.Close()
 }
 
 func (c internalConn) Type() string {

+ 2 - 0
lib/connections/tcp_dial.go

@@ -16,6 +16,8 @@ import (
 	"github.com/syncthing/syncthing/lib/protocol"
 )
 
+const tcpPriority = 10
+
 func init() {
 	factory := &tcpDialerFactory{}
 	for _, scheme := range []string{"tcp", "tcp4", "tcp6"} {

+ 5 - 3
lib/discover/cache.go

@@ -7,6 +7,7 @@
 package discover
 
 import (
+	"sort"
 	stdsync "sync"
 	"time"
 
@@ -121,11 +122,12 @@ func (m *cachingMux) Lookup(deviceID protocol.DeviceID) (addresses []string, err
 	}
 	m.mut.RUnlock()
 
+	addresses = util.UniqueTrimmedStrings(addresses)
+	sort.Strings(addresses)
+
 	l.Debugln("lookup results for", deviceID)
 	l.Debugln("  addresses: ", addresses)
 
-	addresses = util.UniqueStrings(addresses)
-
 	return addresses, nil
 }
 
@@ -185,7 +187,7 @@ func (m *cachingMux) Cache() map[protocol.DeviceID]CacheEntry {
 	m.mut.RUnlock()
 
 	for k, v := range res {
-		v.Addresses = util.UniqueStrings(v.Addresses)
+		v.Addresses = util.UniqueTrimmedStrings(v.Addresses)
 		res[k] = v
 	}
 

+ 2 - 5
lib/model/folder_sendrecv.go

@@ -9,7 +9,6 @@ package model
 import (
 	"bytes"
 	"fmt"
-	"math/rand"
 	"path/filepath"
 	"runtime"
 	"sort"
@@ -25,6 +24,7 @@ import (
 	"github.com/syncthing/syncthing/lib/ignore"
 	"github.com/syncthing/syncthing/lib/osutil"
 	"github.com/syncthing/syncthing/lib/protocol"
+	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/scanner"
 	"github.com/syncthing/syncthing/lib/sha256"
 	"github.com/syncthing/syncthing/lib/sync"
@@ -1089,10 +1089,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
 	}
 
 	// Shuffle the blocks
-	for i := range blocks {
-		j := rand.Intn(i + 1)
-		blocks[i], blocks[j] = blocks[j], blocks[i]
-	}
+	rand.Shuffle(blocks)
 
 	events.Default.Log(events.ItemStarted, map[string]string{
 		"folder": f.folderID,

+ 2 - 6
lib/model/queue.go

@@ -7,10 +7,10 @@
 package model
 
 import (
-	"math/rand"
 	"sort"
 	"time"
 
+	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/sync"
 )
 
@@ -103,11 +103,7 @@ func (q *jobQueue) Shuffle() {
 	q.mut.Lock()
 	defer q.mut.Unlock()
 
-	l := len(q.queued)
-	for i := range q.queued {
-		r := rand.Intn(l)
-		q.queued[i], q.queued[r] = q.queued[r], q.queued[i]
-	}
+	rand.Shuffle(q.queued)
 }
 
 func (q *jobQueue) Reset() {

+ 12 - 0
lib/rand/random.go

@@ -14,6 +14,7 @@ import (
 	"encoding/binary"
 	"io"
 	mathRand "math/rand"
+	"reflect"
 )
 
 // Reader is the standard crypto/rand.Reader, re-exported for convenience
@@ -73,3 +74,14 @@ func SeedFromBytes(bs []byte) int64 {
 	// uint64s and XOR them together.
 	return int64(binary.BigEndian.Uint64(s[0:]) ^ binary.BigEndian.Uint64(s[8:]))
 }
+
+// Shuffle the order of elements
+func Shuffle(slice interface{}) {
+	rv := reflect.ValueOf(slice)
+	swap := reflect.Swapper(slice)
+	length := rv.Len()
+	if length < 2 {
+		return
+	}
+	defaultSecureRand.Shuffle(length, swap)
+}

+ 2 - 9
lib/relay/client/dynamic.go

@@ -6,13 +6,13 @@ import (
 	"crypto/tls"
 	"encoding/json"
 	"fmt"
-	"math/rand"
 	"net/http"
 	"net/url"
 	"sort"
 	"time"
 
 	"github.com/syncthing/syncthing/lib/osutil"
+	"github.com/syncthing/syncthing/lib/rand"
 	"github.com/syncthing/syncthing/lib/relay/protocol"
 	"github.com/syncthing/syncthing/lib/sync"
 )
@@ -209,7 +209,7 @@ func relayAddressesOrder(input []string) []string {
 
 	var ids []int
 	for id, bucket := range buckets {
-		shuffle(bucket)
+		rand.Shuffle(bucket)
 		ids = append(ids, id)
 	}
 
@@ -223,10 +223,3 @@ func relayAddressesOrder(input []string) []string {
 
 	return addresses
 }
-
-func shuffle(slice []string) {
-	for i := len(slice) - 1; i > 0; i-- {
-		j := rand.Intn(i + 1)
-		slice[i], slice[j] = slice[j], slice[i]
-	}
-}

+ 22 - 0
lib/stun/debug.go

@@ -0,0 +1,22 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package stun
+
+import (
+	"os"
+	"strings"
+
+	"github.com/syncthing/syncthing/lib/logger"
+)
+
+var (
+	l = logger.DefaultLogger.NewFacility("stun", "STUN functionality")
+)
+
+func init() {
+	l.SetDebug("stun", strings.Contains(os.Getenv("STTRACE"), "stun") || os.Getenv("STTRACE") == "all")
+}

+ 64 - 0
lib/stun/filter.go

@@ -0,0 +1,64 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package stun
+
+import (
+	"bytes"
+	"net"
+	"sync"
+	"time"
+)
+
+const (
+	stunFilterPriority = 10
+	otherDataPriority  = 100
+)
+
+type stunFilter struct {
+	ids map[string]time.Time
+	mut sync.Mutex
+}
+
+func (f *stunFilter) Outgoing(out []byte, addr net.Addr) {
+	if !f.isStunPayload(out) {
+		panic("not a stun payload")
+	}
+	f.mut.Lock()
+	f.ids[string(out[8:20])] = time.Now().Add(time.Minute)
+	f.reap()
+	f.mut.Unlock()
+}
+
+func (f *stunFilter) ClaimIncoming(in []byte, addr net.Addr) bool {
+	if f.isStunPayload(in) {
+		f.mut.Lock()
+		_, ok := f.ids[string(in[8:20])]
+		f.reap()
+		f.mut.Unlock()
+		return ok
+	}
+	return false
+}
+
+func (f *stunFilter) isStunPayload(data []byte) bool {
+	// Need at least 20 bytes
+	if len(data) < 20 {
+		return false
+	}
+
+	// First two bits always unset, and should always send magic cookie.
+	return data[0]&0xc0 == 0 && bytes.Equal(data[4:8], []byte{0x21, 0x12, 0xA4, 0x42})
+}
+
+func (f *stunFilter) reap() {
+	now := time.Now()
+	for id, timeout := range f.ids {
+		if timeout.Before(now) {
+			delete(f.ids, id)
+		}
+	}
+}

+ 310 - 0
lib/stun/stun.go

@@ -0,0 +1,310 @@
+// Copyright (C) 2019 The Syncthing Authors.
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+package stun
+
+import (
+	"net"
+	"sync/atomic"
+	"time"
+
+	"github.com/AudriusButkevicius/pfilter"
+	"github.com/ccding/go-stun/stun"
+	"github.com/syncthing/syncthing/lib/config"
+)
+
+const stunRetryInterval = 5 * time.Minute
+
+type Host = stun.Host
+type NATType = stun.NATType
+
+// NAT types.
+
+const (
+	NATError                = stun.NATError
+	NATUnknown              = stun.NATUnknown
+	NATNone                 = stun.NATNone
+	NATBlocked              = stun.NATBlocked
+	NATFull                 = stun.NATFull
+	NATSymmetric            = stun.NATSymmetric
+	NATRestricted           = stun.NATRestricted
+	NATPortRestricted       = stun.NATPortRestricted
+	NATSymmetricUDPFirewall = stun.NATSymmetricUDPFirewall
+)
+
+type writeTrackingPacketConn struct {
+	lastWrite int64
+	net.PacketConn
+}
+
+func (c *writeTrackingPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
+	atomic.StoreInt64(&c.lastWrite, time.Now().Unix())
+	return c.PacketConn.WriteTo(p, addr)
+}
+
+func (c *writeTrackingPacketConn) getLastWrite() time.Time {
+	unix := atomic.LoadInt64(&c.lastWrite)
+	return time.Unix(unix, 0)
+}
+
+type Subscriber interface {
+	OnNATTypeChanged(natType NATType)
+	OnExternalAddressChanged(address *Host, via string)
+}
+
+type Service struct {
+	name       string
+	cfg        config.Wrapper
+	subscriber Subscriber
+	stunConn   net.PacketConn
+	client     *stun.Client
+
+	writeTrackingPacketConn *writeTrackingPacketConn
+
+	natType NATType
+	addr    *Host
+
+	stop chan struct{}
+}
+
+func New(cfg config.Wrapper, subscriber Subscriber, conn net.PacketConn) (*Service, net.PacketConn) {
+	// Wrap the original connection to track writes on it
+	writeTrackingPacketConn := &writeTrackingPacketConn{lastWrite: 0, PacketConn: conn}
+
+	// Wrap it in a filter and split it up, so that stun packets arrive on stun conn, others arrive on the data conn
+	filterConn := pfilter.NewPacketFilter(writeTrackingPacketConn)
+	otherDataConn := filterConn.NewConn(otherDataPriority, nil)
+	stunConn := filterConn.NewConn(stunFilterPriority, &stunFilter{
+		ids: make(map[string]time.Time),
+	})
+
+	filterConn.Start()
+
+	// Construct the client to use the stun conn
+	client := stun.NewClientWithConnection(stunConn)
+	client.SetSoftwareName("") // Explicitly unset this, seems to freak some servers out.
+
+	// Return the service and the other conn to the client
+	return &Service{
+		name: "Stun@" + conn.LocalAddr().Network() + "://" + conn.LocalAddr().String(),
+
+		cfg:        cfg,
+		subscriber: subscriber,
+		stunConn:   stunConn,
+		client:     client,
+
+		writeTrackingPacketConn: writeTrackingPacketConn,
+
+		natType: NATUnknown,
+		addr:    nil,
+		stop:    make(chan struct{}),
+	}, otherDataConn
+}
+
+func (s *Service) Stop() {
+	close(s.stop)
+	_ = s.stunConn.Close()
+}
+
+func (s *Service) Serve() {
+	for {
+	disabled:
+		s.setNATType(NATUnknown)
+		s.setExternalAddress(nil, "")
+
+		if s.cfg.Options().IsStunDisabled() {
+			select {
+			case <-s.stop:
+				return
+			case <-time.After(time.Second):
+				continue
+			}
+		}
+
+		l.Debugf("Starting stun for %s", s)
+
+		for _, addr := range s.cfg.StunServers() {
+			// This blocks until we hit an exit condition or there are issues with the STUN server.
+			// This returns a boolean signifying if a different STUN server should be tried (oppose to the whole thing
+			// shutting down and this winding itself down.
+			if !s.runStunForServer(addr) {
+				// Check exit conditions.
+
+				// Have we been asked to stop?
+				select {
+				case <-s.stop:
+					return
+				default:
+				}
+
+				// Are we disabled?
+				if s.cfg.Options().IsStunDisabled() {
+					l.Infoln("STUN disabled")
+					goto disabled
+				}
+
+				// Unpunchable NAT? Chillout for some time.
+				if !s.isCurrentNATTypePunchable() {
+					break
+				}
+			}
+		}
+
+		// Failed all servers, sad.
+		s.setNATType(NATUnknown)
+		s.setExternalAddress(nil, "")
+
+		// We failed to contact all provided stun servers or the nat is not punchable.
+		// Chillout for a while.
+		time.Sleep(stunRetryInterval)
+	}
+}
+
+func (s *Service) runStunForServer(addr string) (tryNext bool) {
+	l.Debugf("Running stun for %s via %s", s, addr)
+
+	// Resolve the address, so that in case the server advertises two
+	// IPs, we always hit the same one, as otherwise, the mapping might
+	// expire as we hit the other address, and cause us to flip flop
+	// between servers/external addresses, as a result flooding discovery
+	// servers.
+	udpAddr, err := net.ResolveUDPAddr("udp", addr)
+	if err != nil {
+		l.Debugf("%s stun addr resolution on %s: %s", s, addr, err)
+		return true
+	}
+	s.client.SetServerAddr(udpAddr.String())
+
+	natType, extAddr, err := s.client.Discover()
+	if err != nil || extAddr == nil {
+		l.Debugf("%s stun discovery on %s: %s", s, addr, err)
+		return true
+	}
+
+	// The stun server is most likely borked, try another one.
+	if natType == NATError || natType == NATUnknown || natType == NATBlocked {
+		l.Debugf("%s stun discovery on %s resolved to %s", s, addr, natType)
+		return true
+	}
+
+	s.setNATType(natType)
+	s.setExternalAddress(extAddr, addr)
+	l.Debugf("%s detected NAT type: %s via %s", s, natType, addr)
+
+	// We can't punch through this one, so no point doing keepalives
+	// and such, just let the caller check the nat type and work it out themselves.
+	if !s.isCurrentNATTypePunchable() {
+		l.Debugf("%s cannot punch %s, skipping", s, natType)
+		return false
+	}
+
+	return s.stunKeepAlive(addr, extAddr)
+}
+
+func (s *Service) stunKeepAlive(addr string, extAddr *Host) (tryNext bool) {
+	var err error
+	nextSleep := time.Duration(s.cfg.Options().StunKeepaliveStartS) * time.Second
+
+	l.Debugf("%s starting stun keepalive via %s, next sleep %s", s, addr, nextSleep)
+
+	for {
+		if areDifferent(s.addr, extAddr) {
+			// If the port has changed (addresses are not equal but the hosts are equal),
+			// we're probably spending too much time between keepalives, reduce the sleep.
+			if s.addr != nil && extAddr != nil && s.addr.IP() == extAddr.IP() {
+				nextSleep /= 2
+				l.Debugf("%s stun port change (%s to %s), next sleep %s", s, s.addr.TransportAddr(), extAddr.TransportAddr(), nextSleep)
+			}
+
+			s.setExternalAddress(extAddr, addr)
+
+			// The stun server is probably stuffed, we've gone beyond min timeout, yet the address keeps changing.
+			minSleep := time.Duration(s.cfg.Options().StunKeepaliveMinS) * time.Second
+			if nextSleep < minSleep {
+				l.Debugf("%s keepalive aborting, sleep below min: %s < %s", s, nextSleep, minSleep)
+				return true
+			}
+		}
+
+		// Adjust the keepalives to fire only nextSleep after last write.
+		lastWrite := s.writeTrackingPacketConn.getLastWrite()
+		minSleep := time.Duration(s.cfg.Options().StunKeepaliveMinS) * time.Second
+		if nextSleep < minSleep {
+			nextSleep = minSleep
+		}
+	tryLater:
+		sleepFor := nextSleep
+
+		timeUntilNextKeepalive := time.Until(lastWrite.Add(sleepFor))
+		if timeUntilNextKeepalive > 0 {
+			sleepFor = timeUntilNextKeepalive
+		}
+
+		l.Debugf("%s stun sleeping for %s", s, sleepFor)
+
+		select {
+		case <-time.After(sleepFor):
+		case <-s.stop:
+			l.Debugf("%s stopping, aborting stun", s)
+			return false
+		}
+
+		if s.cfg.Options().IsStunDisabled() {
+			// Disabled, give up
+			l.Debugf("%s disabled, aborting stun ", s)
+			return false
+		}
+
+		// Check if any writes happened while we were sleeping, if they did, sleep again
+		lastWrite = s.writeTrackingPacketConn.getLastWrite()
+		if gap := time.Since(lastWrite); gap < nextSleep {
+			l.Debugf("%s stun last write gap less than next sleep: %s < %s. Will try later", s, gap, nextSleep)
+			goto tryLater
+		}
+
+		l.Debugf("%s stun keepalive", s)
+
+		extAddr, err = s.client.Keepalive()
+		if err != nil {
+			l.Debugf("%s stun keepalive on %s: %s (%v)", s, addr, err, extAddr)
+			return true
+		}
+	}
+}
+
+func (s *Service) setNATType(natType NATType) {
+	if natType != s.natType {
+		l.Debugf("Notifying %s of NAT type change: %s", s.subscriber, natType)
+		s.subscriber.OnNATTypeChanged(natType)
+	}
+	s.natType = natType
+}
+
+func (s *Service) setExternalAddress(addr *Host, via string) {
+	if areDifferent(s.addr, addr) {
+		l.Debugf("Notifying %s of address change: %s via %s", s.subscriber, addr, via)
+		s.subscriber.OnExternalAddressChanged(addr, via)
+	}
+	s.addr = addr
+}
+
+func (s *Service) String() string {
+	return s.name
+}
+
+func (s *Service) isCurrentNATTypePunchable() bool {
+	return s.natType == NATNone || s.natType == NATPortRestricted || s.natType == NATRestricted || s.natType == NATFull
+}
+
+func areDifferent(first, second *Host) bool {
+	if (first == nil) != (second == nil) {
+		return true
+	}
+	if first != nil {
+		return first.TransportAddr() != second.TransportAddr()
+	}
+	return false
+}

+ 13 - 12
lib/util/utils.go

@@ -10,7 +10,6 @@ import (
 	"fmt"
 	"net/url"
 	"reflect"
-	"sort"
 	"strconv"
 	"strings"
 )
@@ -111,21 +110,23 @@ func CopyMatchingTag(from interface{}, to interface{}, tag string, shouldCopy fu
 	}
 }
 
-// UniqueStrings returns a list on unique strings, trimming and sorting them
-// at the same time.
-func UniqueStrings(ss []string) []string {
-	var m = make(map[string]bool, len(ss))
-	for _, s := range ss {
-		m[strings.Trim(s, " ")] = true
+// UniqueTrimmedStrings returns a list on unique strings, trimming at the same time.
+func UniqueTrimmedStrings(ss []string) []string {
+	// Trim all first
+	for i, v := range ss {
+		ss[i] = strings.Trim(v, " ")
 	}
 
-	var us = make([]string, 0, len(m))
-	for k := range m {
-		us = append(us, k)
+	var m = make(map[string]struct{}, len(ss))
+	var us = make([]string, 0, len(ss))
+	for _, v := range ss {
+		if _, ok := m[v]; ok {
+			continue
+		}
+		m[v] = struct{}{}
+		us = append(us, v)
 	}
 
-	sort.Strings(us)
-
 	return us
 }
 

+ 1 - 5
lib/util/utils_test.go

@@ -74,10 +74,6 @@ func TestUniqueStrings(t *testing.T) {
 			nil,
 			nil,
 		},
-		{
-			[]string{"b", "a"},
-			[]string{"a", "b"},
-		},
 		{
 			[]string{"       a     ", "     a  ", "b        ", "    b"},
 			[]string{"a", "b"},
@@ -85,7 +81,7 @@ func TestUniqueStrings(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		result := UniqueStrings(test.input)
+		result := UniqueTrimmedStrings(test.input)
 		if len(result) != len(test.expected) {
 			t.Errorf("%s != %s", result, test.expected)
 		}

+ 3 - 1
lib/versioner/simple.go

@@ -8,6 +8,7 @@ package versioner
 
 import (
 	"path/filepath"
+	"sort"
 	"strconv"
 	"time"
 
@@ -71,7 +72,8 @@ func (v Simple) Archive(filePath string) error {
 
 	// Use all the found filenames. "~" sorts after "." so all old pattern
 	// files will be deleted before any new, which is as it should be.
-	versions := util.UniqueStrings(append(oldVersions, newVersions...))
+	versions := util.UniqueTrimmedStrings(append(oldVersions, newVersions...))
+	sort.Strings(versions)
 
 	if len(versions) > v.keep {
 		for _, toRemove := range versions[:len(versions)-v.keep] {

+ 4 - 1
lib/versioner/staggered.go

@@ -8,6 +8,7 @@ package versioner
 
 import (
 	"path/filepath"
+	"sort"
 	"strconv"
 	"time"
 
@@ -242,7 +243,9 @@ func (v *Staggered) Archive(filePath string) error {
 
 	// Use all the found filenames.
 	versions := append(oldVersions, newVersions...)
-	v.expire(util.UniqueStrings(versions))
+	versions = util.UniqueTrimmedStrings(versions)
+	sort.Strings(versions)
+	v.expire(versions)
 
 	return nil
 }