From e714df013f49a0fbfc84bc4b1a954b25bb0f9747 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Wed, 29 May 2019 08:56:40 +0100 Subject: [PATCH] lib/connections: Add QUIC protocol support (fixes #5377) (#5737) --- cmd/strelaypoolsrv/main.go | 7 +- go.mod | 7 +- go.sum | 60 +++-- lib/api/mocked_config_test.go | 28 +- lib/config/config.go | 31 ++- lib/config/config_test.go | 10 +- lib/config/optionsconfiguration.go | 39 +-- lib/config/testdata/overridenvalues.xml | 3 + lib/config/wrapper.go | 30 ++- lib/connections/config.go | 12 - lib/connections/quic_dial.go | 128 +++++++++ lib/connections/quic_listen.go | 238 +++++++++++++++++ lib/connections/quic_misc.go | 57 ++++ lib/connections/quic_misc_test.go | 92 +++++++ lib/connections/registry/registry.go | 89 +++++++ lib/connections/registry/registry_test.go | 71 +++++ lib/connections/relay_dial.go | 2 + lib/connections/service.go | 6 +- lib/connections/structs.go | 24 +- lib/connections/tcp_dial.go | 2 + lib/discover/cache.go | 8 +- lib/model/folder_sendrecv.go | 7 +- lib/model/queue.go | 8 +- lib/rand/random.go | 12 + lib/relay/client/dynamic.go | 11 +- lib/stun/debug.go | 22 ++ lib/stun/filter.go | 64 +++++ lib/stun/stun.go | 310 ++++++++++++++++++++++ lib/util/utils.go | 25 +- lib/util/utils_test.go | 6 +- lib/versioner/simple.go | 4 +- lib/versioner/staggered.go | 5 +- 32 files changed, 1290 insertions(+), 128 deletions(-) delete mode 100644 lib/connections/config.go create mode 100644 lib/connections/quic_dial.go create mode 100644 lib/connections/quic_listen.go create mode 100644 lib/connections/quic_misc.go create mode 100644 lib/connections/quic_misc_test.go create mode 100644 lib/connections/registry/registry.go create mode 100644 lib/connections/registry/registry_test.go create mode 100644 lib/stun/debug.go create mode 100644 lib/stun/filter.go create mode 100644 lib/stun/stun.go diff --git a/cmd/strelaypoolsrv/main.go b/cmd/strelaypoolsrv/main.go index 363c2bcb4..1f0c9c323 100644 --- a/cmd/strelaypoolsrv/main.go +++ b/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, diff --git a/go.mod b/go.mod index d300429e4..3b367275f 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index f0f6307a8..a2b7c6e2d 100644 --- a/go.sum +++ b/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= diff --git a/lib/api/mocked_config_test.go b/lib/api/mocked_config_test.go index cbf8c7223..60ddc0955 100644 --- a/lib/api/mocked_config_test.go +++ b/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 } diff --git a/lib/config/config.go b/lib/config/config.go index 9e1ead571..04e460a46 100644 --- a/lib/config/config.go +++ b/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 default. 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 default. + + // 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) diff --git a/lib/config/config_test.go b/lib/config/config_test.go index ee6142d8d..a227abffa 100644 --- a/lib/config/config_test.go +++ b/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") diff --git a/lib/config/optionsconfiguration.go b/lib/config/optionsconfiguration.go index 22e7a6cc9..e4da60481 100644 --- a/lib/config/optionsconfiguration.go +++ b/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, ©, "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 } diff --git a/lib/config/testdata/overridenvalues.xml b/lib/config/testdata/overridenvalues.xml index 315f464a1..9e3836d36 100644 --- a/lib/config/testdata/overridenvalues.xml +++ b/lib/config/testdata/overridenvalues.xml @@ -36,5 +36,8 @@ 100 /media/syncthing false + 9000 + 900 + foo diff --git a/lib/config/wrapper.go b/lib/config/wrapper.go index dabcd581a..edd377577 100644 --- a/lib/config/wrapper.go +++ b/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 { diff --git a/lib/connections/config.go b/lib/connections/config.go deleted file mode 100644 index 659b39234..000000000 --- a/lib/connections/config.go +++ /dev/null @@ -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 -) diff --git a/lib/connections/quic_dial.go b/lib/connections/quic_dial.go new file mode 100644 index 000000000..88ff5fa1d --- /dev/null +++ b/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" +} diff --git a/lib/connections/quic_listen.go b/lib/connections/quic_listen.go new file mode 100644 index 000000000..a192008ca --- /dev/null +++ b/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 +} diff --git a/lib/connections/quic_misc.go b/lib/connections/quic_misc.go new file mode 100644 index 000000000..93c086008 --- /dev/null +++ b/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 +} diff --git a/lib/connections/quic_misc_test.go b/lib/connections/quic_misc_test.go new file mode 100644 index 000000000..726e5e20f --- /dev/null +++ b/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") + } + } +} diff --git a/lib/connections/registry/registry.go b/lib/connections/registry/registry.go new file mode 100644 index 000000000..42dcf699b --- /dev/null +++ b/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) +} diff --git a/lib/connections/registry/registry_test.go b/lib/connections/registry/registry_test.go new file mode 100644 index 000000000..f62a5f5c7 --- /dev/null +++ b/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 +} diff --git a/lib/connections/relay_dial.go b/lib/connections/relay_dial.go index ab236e289..3ba5558b2 100644 --- a/lib/connections/relay_dial.go +++ b/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{} } diff --git a/lib/connections/service.go b/lib/connections/service.go index 95547f2e2..02b6b15bd 100644 --- a/lib/connections/service.go +++ b/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 { diff --git a/lib/connections/structs.go b/lib/connections/structs.go index cdb14c08c..8cffbaff1 100644 --- a/lib/connections/structs.go +++ b/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 { diff --git a/lib/connections/tcp_dial.go b/lib/connections/tcp_dial.go index 618e772bf..db083e45f 100644 --- a/lib/connections/tcp_dial.go +++ b/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"} { diff --git a/lib/discover/cache.go b/lib/discover/cache.go index 2fb281696..230d89fdc 100644 --- a/lib/discover/cache.go +++ b/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 } diff --git a/lib/model/folder_sendrecv.go b/lib/model/folder_sendrecv.go index 584a61e5b..c01b34cf6 100644 --- a/lib/model/folder_sendrecv.go +++ b/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, diff --git a/lib/model/queue.go b/lib/model/queue.go index e52879820..15d13914b 100644 --- a/lib/model/queue.go +++ b/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() { diff --git a/lib/rand/random.go b/lib/rand/random.go index 23a2f255e..262669e02 100644 --- a/lib/rand/random.go +++ b/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) +} diff --git a/lib/relay/client/dynamic.go b/lib/relay/client/dynamic.go index 5e83c5144..17400b788 100644 --- a/lib/relay/client/dynamic.go +++ b/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] - } -} diff --git a/lib/stun/debug.go b/lib/stun/debug.go new file mode 100644 index 000000000..196dccef3 --- /dev/null +++ b/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") +} diff --git a/lib/stun/filter.go b/lib/stun/filter.go new file mode 100644 index 000000000..8be3bd4de --- /dev/null +++ b/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) + } + } +} diff --git a/lib/stun/stun.go b/lib/stun/stun.go new file mode 100644 index 000000000..bcf18e75a --- /dev/null +++ b/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 +} diff --git a/lib/util/utils.go b/lib/util/utils.go index 15fb6db57..c2d9296f4 100644 --- a/lib/util/utils.go +++ b/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 } diff --git a/lib/util/utils_test.go b/lib/util/utils_test.go index 837d45bf4..268c27866 100644 --- a/lib/util/utils_test.go +++ b/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) } diff --git a/lib/versioner/simple.go b/lib/versioner/simple.go index 43f55ce01..cd9ef97f9 100644 --- a/lib/versioner/simple.go +++ b/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] { diff --git a/lib/versioner/staggered.go b/lib/versioner/staggered.go index 624959723..e9ac308e0 100644 --- a/lib/versioner/staggered.go +++ b/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 }