diff --git a/cmd/strelaypoolsrv/LICENSE b/cmd/strelaypoolsrv/LICENSE new file mode 100644 index 00000000..581a1705 --- /dev/null +++ b/cmd/strelaypoolsrv/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 The Syncthing Project + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/cmd/strelaypoolsrv/README.md b/cmd/strelaypoolsrv/README.md new file mode 100644 index 00000000..e8001631 --- /dev/null +++ b/cmd/strelaypoolsrv/README.md @@ -0,0 +1,15 @@ +# relaypoolsrv + +[![Latest Build](http://img.shields.io/jenkins/s/http/build.syncthing.net/relaypoolsrv.svg?style=flat-square)](http://build.syncthing.net/job/relaypoolsrv/lastBuild/) + +This is the relay pool server for the `syncthing` project, which allows community hosted [relaysrv](https://github.com/syncthing/relaysrv)'s to join the public pool. + +Servers that join the pool are then advertised to users of `syncthing` as potential connection points for those who are unable to connect directly due to NAT or firewall issues. + +There is very little reason why you'd want to run this yourself, as `relaypoolsrv` is just used for announcement and lookup of public relay servers. If you are looking to setup a private or a public relay, please check the documentation for [relaysrv](https://github.com/syncthing/relaysrv), which also explains how to join the default public pool. + +If you still want to run it, you can run `go get github.com/syncthing/relaypoolsrv` download it or download the +[latest build](http://build.syncthing.net/job/relaypoolsrv/lastSuccessfulBuild/artifact/) +from the build server. + +See `relaypoolsrv -help` for configuration options. diff --git a/cmd/strelaypoolsrv/auto/gui.go b/cmd/strelaypoolsrv/auto/gui.go new file mode 100644 index 00000000..b43e8c74 --- /dev/null +++ b/cmd/strelaypoolsrv/auto/gui.go @@ -0,0 +1,16 @@ +package auto + +import ( + "encoding/base64" +) + +const ( + AssetsBuildDate = "Thu, 14 Apr 2016 21:31:07 GMT" +) + +func Assets() map[string][]byte { + var assets = make(map[string][]byte, 1) + + assets["index.html"], _ = base64.StdEncoding.DecodeString("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") + return assets +} diff --git a/cmd/strelaypoolsrv/gui/index.html b/cmd/strelaypoolsrv/gui/index.html new file mode 100644 index 00000000..8b6ca622 --- /dev/null +++ b/cmd/strelaypoolsrv/gui/index.html @@ -0,0 +1,395 @@ + + + + + + + + + + + Relay stats + + + + + + + +
+

Relay Pool Data

+
+ +

Please wait while we gather data

+
+
+
+

+ Currently {{ relays.length }} relays online ({{ totals.goMaxProcs }} cores in total). +

+
+
+

The circle size represents how much bytes the relay transfered relative to other relays

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Address + + + Sessions + + + + + + Connections + + + + + + Data relayed + + + + Transfer rate in the last period + + Uptime hours + + + + + + Provided by + + + +
+ + 10s + + + + + + 1m + + + + + + 5m + + + + + + 15m + + + + + + 30m + + + + + + 60m + + + +
{{ relay.address }}Looking up...{{ relay.status.numActiveSessions }}{{ relay.status.numConnections }}{{ relay.status.bytesProxied | bytes }}{{ relay.status.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s{{ relay.status.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s{{ relay.status.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s{{ relay.status.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s{{ relay.status.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s{{ relay.status.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s{{ relay.status.uptimeSeconds/60/60 | number:0 }} + {{ relay.status.options['provided-by'] || '' | limitTo:50 }} + … +
Totals{{ totals.numActiveSessions }}{{ totals.numConnections }}{{ totals.bytesProxied | bytes }}{{ totals.kbps10s1m5m15m30m60m[0] * 128 | bytes }}/s{{ totals.kbps10s1m5m15m30m60m[1] * 128 | bytes }}/s{{ totals.kbps10s1m5m15m30m60m[2] * 128 | bytes }}/s{{ totals.kbps10s1m5m15m30m60m[3] * 128 | bytes }}/s{{ totals.kbps10s1m5m15m30m60m[4] * 128 | bytes }}/s{{ totals.kbps10s1m5m15m30m60m[5] * 128 | bytes }}/s{{ totals.uptimeSeconds/60/60 | number:0 }} hours{{ relays.length }} relays
+
+
+

+ This product includes GeoLite2 data created by MaxMind, available from + http://www.maxmind.com. +

+
+ + + + + + + + + + + + diff --git a/cmd/strelaypoolsrv/main.go b/cmd/strelaypoolsrv/main.go new file mode 100644 index 00000000..ddeeed5d --- /dev/null +++ b/cmd/strelaypoolsrv/main.go @@ -0,0 +1,543 @@ +// Copyright (C) 2015 Audrius Butkevicius and Contributors (see the CONTRIBUTORS file). + +//go:generate go run genassets.go gui auto/gui.go + +package main + +import ( + "bytes" + "compress/gzip" + "crypto/tls" + "encoding/json" + "flag" + "fmt" + "io/ioutil" + "log" + "math/rand" + "mime" + "net" + "net/http" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/golang/groupcache/lru" + "github.com/juju/ratelimit" + + "github.com/oschwald/geoip2-golang" + + "github.com/syncthing/relaypoolsrv/auto" + "github.com/syncthing/syncthing/lib/relay/client" + "github.com/syncthing/syncthing/lib/sync" + "github.com/syncthing/syncthing/lib/tlsutil" +) + +type location struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +type relay struct { + URL string `json:"url"` + Location location `json:"location"` + uri *url.URL +} + +func (r relay) String() string { + return r.URL +} + +type request struct { + relay relay + uri *url.URL + result chan result +} + +type result struct { + err error + eviction time.Duration +} + +var ( + binDir string + testCert tls.Certificate + listen string = ":80" + dir string = "" + evictionTime time.Duration = time.Hour + debug bool = false + getLRUSize int = 10 << 10 + getLimitBurst int64 = 10 + getLimitAvg = 1 + postLRUSize int = 1 << 10 + postLimitBurst int64 = 2 + postLimitAvg = 1 + getLimit time.Duration + postLimit time.Duration + permRelaysFile string + ipHeader string + geoipPath string + + getMut sync.RWMutex = sync.NewRWMutex() + getLRUCache *lru.Cache + + postMut sync.RWMutex = sync.NewRWMutex() + postLRUCache *lru.Cache + + requests = make(chan request, 10) + + mut sync.RWMutex = sync.NewRWMutex() + knownRelays []relay = make([]relay, 0) + permanentRelays []relay = make([]relay, 0) + evictionTimers map[string]*time.Timer = make(map[string]*time.Timer) +) + +func main() { + flag.StringVar(&listen, "listen", listen, "Listen address") + flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening") + flag.BoolVar(&debug, "debug", debug, "Enable debug output") + flag.DurationVar(&evictionTime, "eviction", evictionTime, "After how long the relay is evicted") + flag.IntVar(&getLRUSize, "get-limit-cache", getLRUSize, "Get request limiter cache size") + flag.IntVar(&getLimitAvg, "get-limit-avg", 2, "Allowed average get request rate, per 10 s") + flag.Int64Var(&getLimitBurst, "get-limit-burst", getLimitBurst, "Allowed burst get requests") + flag.IntVar(&postLRUSize, "post-limit-cache", postLRUSize, "Post request limiter cache size") + flag.IntVar(&postLimitAvg, "post-limit-avg", 2, "Allowed average post request rate, per minute") + flag.Int64Var(&postLimitBurst, "post-limit-burst", postLimitBurst, "Allowed burst post requests") + flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays") + flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.") + flag.StringVar(&geoipPath, "geoip", "GeoLite2-City.mmdb", "Path to GeoLite2-City database") + + flag.Parse() + + getLimit = 10 * time.Second / time.Duration(getLimitAvg) + postLimit = time.Minute / time.Duration(postLimitAvg) + + getLRUCache = lru.New(getLRUSize) + postLRUCache = lru.New(postLRUSize) + + var listener net.Listener + var err error + + if permRelaysFile != "" { + loadPermanentRelays(permRelaysFile) + } + + testCert = createTestCertificate() + + go requestProcessor() + + if dir != "" { + if debug { + log.Println("Starting TLS listener on", listen) + } + certFile, keyFile := filepath.Join(dir, "http-cert.pem"), filepath.Join(dir, "http-key.pem") + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + log.Fatalln("Failed to load HTTP X509 key pair:", err) + } + + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS10, // No SSLv3 + CipherSuites: []uint16{ + // No RC4 + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, + tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, + tls.TLS_RSA_WITH_AES_128_CBC_SHA, + tls.TLS_RSA_WITH_AES_256_CBC_SHA, + tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, + tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, + }, + } + + listener, err = tls.Listen("tcp", listen, tlsCfg) + } else { + if debug { + log.Println("Starting plain listener on", listen) + } + listener, err = net.Listen("tcp", listen) + } + + if err != nil { + log.Fatalln("listen:", err) + } + + handler := http.NewServeMux() + handler.HandleFunc("/", handleAssets) + handler.HandleFunc("/endpoint", handleRequest) + + srv := http.Server{ + Handler: handler, + ReadTimeout: 10 * time.Second, + } + + err = srv.Serve(listener) + if err != nil { + log.Fatalln("serve:", err) + } +} + +func handleAssets(w http.ResponseWriter, r *http.Request) { + assets := auto.Assets() + path := r.URL.Path[1:] + if path == "" { + path = "index.html" + } + + bs, ok := assets[path] + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + + if r.Header.Get("If-Modified-Since") == auto.AssetsBuildDate { + w.WriteHeader(http.StatusNotModified) + return + } + + mtype := mimeTypeForFile(path) + if len(mtype) != 0 { + w.Header().Set("Content-Type", mtype) + } + + if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { + w.Header().Set("Content-Encoding", "gzip") + } else { + // ungzip if browser not send gzip accepted header + var gr *gzip.Reader + gr, _ = gzip.NewReader(bytes.NewReader(bs)) + bs, _ = ioutil.ReadAll(gr) + gr.Close() + } + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs))) + w.Header().Set("Last-Modified", auto.AssetsBuildDate) + w.Header().Set("Cache-Control", "public") + + w.Write(bs) +} + +func mimeTypeForFile(file string) string { + // We use a built in table of the common types since the system + // TypeByExtension might be unreliable. But if we don't know, we delegate + // to the system. + ext := filepath.Ext(file) + switch ext { + case ".htm", ".html": + return "text/html" + case ".css": + return "text/css" + case ".js": + return "application/javascript" + case ".json": + return "application/json" + case ".png": + return "image/png" + case ".ttf": + return "application/x-font-ttf" + case ".woff": + return "application/x-font-woff" + case ".svg": + return "image/svg+xml" + default: + return mime.TypeByExtension(ext) + } +} + +func handleRequest(w http.ResponseWriter, r *http.Request) { + if ipHeader != "" { + r.RemoteAddr = r.Header.Get(ipHeader) + } + w.Header().Set("Access-Control-Allow-Origin", "*") + switch r.Method { + case "GET": + if limit(r.RemoteAddr, getLRUCache, getMut, getLimit, int64(getLimitBurst)) { + w.WriteHeader(429) + return + } + handleGetRequest(w, r) + case "POST": + if limit(r.RemoteAddr, postLRUCache, postMut, postLimit, int64(postLimitBurst)) { + w.WriteHeader(429) + return + } + handlePostRequest(w, r) + default: + if debug { + log.Println("Unhandled HTTP method", r.Method) + } + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } +} + +func handleGetRequest(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + mut.RLock() + relays := append(permanentRelays, knownRelays...) + mut.RUnlock() + + // Shuffle + for i := range relays { + j := rand.Intn(i + 1) + relays[i], relays[j] = relays[j], relays[i] + } + + json.NewEncoder(w).Encode(map[string][]relay{ + "relays": relays, + }) +} + +func handlePostRequest(w http.ResponseWriter, r *http.Request) { + var newRelay relay + err := json.NewDecoder(r.Body).Decode(&newRelay) + r.Body.Close() + + if err != nil { + if debug { + log.Println("Failed to parse payload") + } + http.Error(w, err.Error(), 500) + return + } + + uri, err := url.Parse(newRelay.URL) + if err != nil { + if debug { + log.Println("Failed to parse URI", newRelay.URL) + } + http.Error(w, err.Error(), 500) + return + } + + host, port, err := net.SplitHostPort(uri.Host) + if err != nil { + if debug { + log.Println("Failed to split URI", newRelay.URL) + } + http.Error(w, err.Error(), 500) + return + } + + // Get the IP address of the client + rhost, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + if debug { + log.Println("Failed to split remote address", r.RemoteAddr) + } + http.Error(w, err.Error(), 500) + return + } + + // The client did not provide an IP address, use the IP address of the client. + if host == "" { + uri.Host = net.JoinHostPort(rhost, port) + newRelay.URL = uri.String() + } else if host != rhost { + if debug { + log.Println("IP address advertised does not match client IP address", r.RemoteAddr, uri) + } + http.Error(w, "IP address does not match client IP", http.StatusUnauthorized) + return + } + newRelay.uri = uri + newRelay.Location = getLocation(uri.Host) + + for _, current := range permanentRelays { + if current.uri.Host == newRelay.uri.Host { + if debug { + log.Println("Asked to add a relay", newRelay, "which exists in permanent list") + } + http.Error(w, "Invalid request", 500) + return + } + } + + reschan := make(chan result) + + select { + case requests <- request{newRelay, uri, reschan}: + result := <-reschan + if result.err != nil { + http.Error(w, result.err.Error(), 500) + return + } + w.Header().Set("Content-Type", "application/json; charset=utf-8") + json.NewEncoder(w).Encode(map[string]time.Duration{ + "evictionIn": result.eviction, + }) + + default: + if debug { + log.Println("Dropping request") + } + w.WriteHeader(429) + } +} + +func requestProcessor() { + for request := range requests { + if debug { + log.Println("Request for", request.relay) + } + if !client.TestRelay(request.uri, []tls.Certificate{testCert}, time.Second, 2*time.Second, 3) { + if debug { + log.Println("Test for relay", request.relay, "failed") + } + request.result <- result{fmt.Errorf("test failed"), 0} + continue + } + + mut.Lock() + timer, ok := evictionTimers[request.relay.uri.Host] + if ok { + if debug { + log.Println("Stopping existing timer for", request.relay) + } + timer.Stop() + } + + for i, current := range knownRelays { + if current.uri.Host == request.relay.uri.Host { + if debug { + log.Println("Relay", request.relay, "already exists") + } + + // Evict the old entry anyway, as configuration might have changed. + last := len(knownRelays) - 1 + knownRelays[i] = knownRelays[last] + knownRelays = knownRelays[:last] + + goto found + } + } + + if debug { + log.Println("Adding new relay", request.relay) + } + + found: + + knownRelays = append(knownRelays, request.relay) + + evictionTimers[request.relay.uri.Host] = time.AfterFunc(evictionTime, evict(request.relay)) + mut.Unlock() + request.result <- result{nil, evictionTime} + } + +} + +func evict(relay relay) func() { + return func() { + mut.Lock() + defer mut.Unlock() + if debug { + log.Println("Evicting", relay) + } + for i, current := range knownRelays { + if current.uri.Host == relay.uri.Host { + if debug { + log.Println("Evicted", relay) + } + last := len(knownRelays) - 1 + knownRelays[i] = knownRelays[last] + knownRelays = knownRelays[:last] + } + } + delete(evictionTimers, relay.uri.Host) + } +} + +func limit(addr string, cache *lru.Cache, lock sync.RWMutex, rate time.Duration, burst int64) bool { + host, _, err := net.SplitHostPort(addr) + if err != nil { + return false + } + + lock.RLock() + bkt, ok := cache.Get(host) + lock.RUnlock() + if ok { + bkt := bkt.(*ratelimit.Bucket) + if bkt.TakeAvailable(1) != 1 { + // Rate limit + return true + } + } else { + lock.Lock() + cache.Add(host, ratelimit.NewBucket(rate, burst)) + lock.Unlock() + } + return false +} + +func loadPermanentRelays(file string) { + content, err := ioutil.ReadFile(file) + if err != nil { + log.Fatal(err) + } + + for _, line := range strings.Split(string(content), "\n") { + if len(line) == 0 { + continue + } + + uri, err := url.Parse(line) + if err != nil { + if debug { + log.Println("Skipping permanent relay", line, "due to parse error", err) + } + continue + + } + + permanentRelays = append(permanentRelays, relay{ + URL: line, + Location: getLocation(uri.Host), + uri: uri, + }) + if debug { + log.Println("Adding permanent relay", line) + } + } +} + +func createTestCertificate() tls.Certificate { + tmpDir, err := ioutil.TempDir("", "relaypoolsrv") + if err != nil { + log.Fatal(err) + } + + certFile, keyFile := filepath.Join(tmpDir, "cert.pem"), filepath.Join(tmpDir, "key.pem") + cert, err := tlsutil.NewCertificate(certFile, keyFile, "relaypoolsrv", 3072) + if err != nil { + log.Fatalln("Failed to create test X509 key pair:", err) + } + + return cert +} + +func getLocation(host string) location { + db, err := geoip2.Open(geoipPath) + if err != nil { + return location{} + } + defer db.Close() + + addr, err := net.ResolveTCPAddr("tcp", host) + if err != nil { + return location{} + } + + city, err := db.City(addr.IP) + if err != nil { + return location{} + } + + return location{ + Latitude: city.Location.Latitude, + Longitude: city.Location.Longitude, + } +}