Browse Source

cmd/strelaysrv: Add uPNP support, ability to set listen protocol (fixes #3503, fixes #3505, fixes #3506)

Audrius Butkevicius 9 years ago
parent
commit
be38c2111f
3 changed files with 84 additions and 13 deletions
  1. 10 4
      cmd/strelaysrv/listener.go
  2. 68 6
      cmd/strelaysrv/main.go
  3. 6 3
      cmd/strelaysrv/pool.go

+ 10 - 4
cmd/strelaysrv/listener.go

@@ -23,7 +23,7 @@ var (
 	numConnections int64
 )
 
-func listener(addr string, config *tls.Config) {
+func listener(proto, addr string, config *tls.Config) {
 	tcpListener, err := net.Listen("tcp", addr)
 	if err != nil {
 		log.Fatalln(err)
@@ -167,10 +167,16 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
 					continue
 				}
 
-				peerOutbox <- serverInvitation
+				select {
+				case peerOutbox <- serverInvitation:
+					if debug {
+						log.Println("Sent invitation from", id, "to", requestedPeer)
+					}
+				default:
+					if debug {
+						log.Println("Could not send invitation from", id, "to", requestedPeer, "as peer disconnected")
+					}
 
-				if debug {
-					log.Println("Sent invitation from", id, "to", requestedPeer)
 				}
 				conn.Close()
 

+ 68 - 6
cmd/strelaysrv/main.go

@@ -24,6 +24,11 @@ import (
 	"github.com/syncthing/syncthing/lib/relay/protocol"
 	"github.com/syncthing/syncthing/lib/tlsutil"
 
+	"github.com/syncthing/syncthing/lib/config"
+	"github.com/syncthing/syncthing/lib/nat"
+	_ "github.com/syncthing/syncthing/lib/pmp"
+	_ "github.com/syncthing/syncthing/lib/upnp"
+
 	syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
 )
 
@@ -48,6 +53,7 @@ func init() {
 var (
 	listen string
 	debug  bool
+	proto  string
 
 	sessionAddress []byte
 	sessionPort    uint16
@@ -70,12 +76,17 @@ var (
 	pools            []string
 	providedBy       string
 	defaultPoolAddrs = "https://relays.syncthing.net/endpoint"
+
+	natEnabled bool
+	natLease   int
+	natRenewal int
+	natTimeout int
 )
 
 func main() {
 	log.SetFlags(log.Lshortfile | log.LstdFlags)
 
-	var dir, extAddress string
+	var dir, extAddress, proto string
 
 	flag.StringVar(&listen, "listen", ":22067", "Protocol listen address")
 	flag.StringVar(&dir, "keys", ".", "Directory where cert.pem and key.pem is stored")
@@ -89,14 +100,22 @@ func main() {
 	flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
 	flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
 	flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertise as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
-
+	flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
+	flag.BoolVar(&natEnabled, "nat", false, "Use UPnP/NAT-PMP to acquire external port mapping")
+	flag.IntVar(&natLease, "nat-lease", 60, "NAT lease length in minutes")
+	flag.IntVar(&natRenewal, "nat-renewal", 30, "NAT renewal frequency in minutes")
+	flag.IntVar(&natTimeout, "nat-timeout", 10, "NAT discovery timeout in seconds")
 	flag.Parse()
 
 	if extAddress == "" {
 		extAddress = listen
 	}
 
-	addr, err := net.ResolveTCPAddr("tcp", extAddress)
+	if len(providedBy) > 30 {
+		log.Fatal("Provided-by cannot be longer than 30 characters")
+	}
+
+	addr, err := net.ResolveTCPAddr(proto, extAddress)
 	if err != nil {
 		log.Fatal(err)
 	}
@@ -149,6 +168,37 @@ func main() {
 		log.Println("ID:", id)
 	}
 
+	wrapper := config.Wrap("config", config.New(id))
+	wrapper.SetOptions(config.OptionsConfiguration{
+		NATLeaseM:   natLease,
+		NATRenewalM: natRenewal,
+		NATTimeoutS: natTimeout,
+	})
+	natSvc := nat.NewService(id, wrapper)
+	mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}
+
+	if natEnabled {
+		go natSvc.Serve()
+		found := make(chan struct{})
+		mapping.OnChanged(func(_ *nat.Mapping, _, _ []nat.Address) {
+			select {
+			case found <- struct{}{}:
+			default:
+			}
+		})
+
+		// Need to wait a few extra seconds, since NAT library waits exactly natTimeout seconds on all interfaces.
+		timeout := time.Duration(natTimeout+2) * time.Second
+		log.Printf("Waiting %s to acquire NAT mapping", timeout)
+
+		select {
+		case <-found:
+			log.Printf("Found NAT mapping: %s", mapping.ExternalAddresses())
+		case <-time.After(timeout):
+			log.Println("Timeout out waiting for NAT mapping.")
+		}
+	}
+
 	if sessionLimitBps > 0 {
 		sessionLimiter = ratelimit.NewBucketWithRate(float64(sessionLimitBps), int64(2*sessionLimitBps))
 	}
@@ -160,7 +210,7 @@ func main() {
 		go statusService(statusAddr)
 	}
 
-	uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", extAddress, id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
+	uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", mapping.Address(), id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
 	if err != nil {
 		log.Fatalln("Failed to construct URI", err)
 	}
@@ -178,11 +228,11 @@ func main() {
 	for _, pool := range pools {
 		pool = strings.TrimSpace(pool)
 		if len(pool) > 0 {
-			go poolHandler(pool, uri)
+			go poolHandler(pool, uri, mapping)
 		}
 	}
 
-	go listener(listen, tlsCfg)
+	go listener(proto, listen, tlsCfg)
 
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
@@ -222,3 +272,15 @@ func monitorLimits() {
 		limitCheckTimer.Reset(time.Minute)
 	}
 }
+
+type mapping struct {
+	*nat.Mapping
+}
+
+func (m *mapping) Address() nat.Address {
+	ext := m.ExternalAddresses()
+	if len(ext) > 0 {
+		return ext[0]
+	}
+	return m.Mapping.Address()
+}

+ 6 - 3
cmd/strelaysrv/pool.go

@@ -12,16 +12,19 @@ import (
 	"time"
 )
 
-func poolHandler(pool string, uri *url.URL) {
+func poolHandler(pool string, uri *url.URL, mapping mapping) {
 	if debug {
 		log.Println("Joining", pool)
 	}
 	for {
+		uriCopy := *uri
+		uriCopy.Host = mapping.Address().String()
+
 		var b bytes.Buffer
 		json.NewEncoder(&b).Encode(struct {
 			URL string `json:"url"`
 		}{
-			uri.String(),
+			uriCopy.String(),
 		})
 
 		resp, err := http.Post(pool, "application/json", &b)
@@ -39,7 +42,7 @@ func poolHandler(pool string, uri *url.URL) {
 			log.Println(pool, "under load, will retry in a minute")
 			time.Sleep(time.Minute)
 			continue
-		} else if resp.StatusCode == 403 {
+		} else if resp.StatusCode == 401 {
 			log.Println(pool, "failed to join due to IP address not matching external address. Aborting")
 			return
 		} else if resp.StatusCode == 200 {