main.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. package main
  2. import (
  3. "crypto/sha1"
  4. "crypto/tls"
  5. "fmt"
  6. "log"
  7. "net"
  8. "net/http"
  9. _ "net/http/pprof"
  10. "os"
  11. "path"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/calmh/ini"
  16. "github.com/calmh/syncthing/discover"
  17. flags "github.com/calmh/syncthing/github.com/jessevdk/go-flags"
  18. "github.com/calmh/syncthing/protocol"
  19. )
  20. type Options struct {
  21. ConfDir string `short:"c" long:"cfg" description:"Configuration directory" default:"~/.syncthing" value-name:"DIR"`
  22. Listen string `short:"l" long:"listen" description:"Listen address" default:":22000" value-name:"ADDR"`
  23. ReadOnly bool `short:"r" long:"ro" description:"Repository is read only"`
  24. Delete bool `short:"d" long:"delete" description:"Delete files deleted from cluster"`
  25. NoSymlinks bool `long:"no-symlinks" description:"Don't follow first level symlinks in the repo"`
  26. Discovery DiscoveryOptions `group:"Discovery Options"`
  27. Advanced AdvancedOptions `group:"Advanced Options"`
  28. Debug DebugOptions `group:"Debugging Options"`
  29. }
  30. type DebugOptions struct {
  31. LogSource bool `long:"log-source"`
  32. TraceFile bool `long:"trace-file"`
  33. TraceNet bool `long:"trace-net"`
  34. TraceIdx bool `long:"trace-idx"`
  35. Profiler string `long:"profiler" value-name:"ADDR"`
  36. }
  37. type DiscoveryOptions struct {
  38. ExternalServer string `long:"ext-server" description:"External discovery server" value-name:"NAME" default:"syncthing.nym.se"`
  39. ExternalPort int `short:"e" long:"ext-port" description:"External listen port" value-name:"PORT" default:"22000"`
  40. NoExternalDiscovery bool `short:"n" long:"no-ext-announce" description:"Do not announce presence externally"`
  41. NoLocalDiscovery bool `short:"N" long:"no-local-announce" description:"Do not announce presence locally"`
  42. }
  43. type AdvancedOptions struct {
  44. RequestsInFlight int `long:"reqs-in-flight" description:"Parallell in flight requests per file" default:"4" value-name:"REQS"`
  45. FilesInFlight int `long:"files-in-flight" description:"Parallell in flight file pulls" default:"8" value-name:"FILES"`
  46. ScanInterval time.Duration `long:"scan-intv" description:"Repository scan interval" default:"60s" value-name:"INTV"`
  47. ConnInterval time.Duration `long:"conn-intv" description:"Node reconnect interval" default:"60s" value-name:"INTV"`
  48. }
  49. var opts Options
  50. var Version string = "unknown-dev"
  51. const (
  52. confDirName = ".syncthing"
  53. confFileName = "syncthing.ini"
  54. )
  55. var (
  56. config ini.Config
  57. nodeAddrs = make(map[string][]string)
  58. )
  59. func main() {
  60. _, err := flags.Parse(&opts)
  61. if err != nil {
  62. os.Exit(0)
  63. }
  64. if opts.Debug.TraceFile || opts.Debug.TraceIdx || opts.Debug.TraceNet || opts.Debug.LogSource {
  65. logger = log.New(os.Stderr, "", log.Lshortfile|log.Ldate|log.Ltime|log.Lmicroseconds)
  66. }
  67. if strings.HasPrefix(opts.ConfDir, "~/") {
  68. opts.ConfDir = strings.Replace(opts.ConfDir, "~", getHomeDir(), 1)
  69. }
  70. infoln("Version", Version)
  71. // Ensure that our home directory exists and that we have a certificate and key.
  72. ensureDir(opts.ConfDir, 0700)
  73. cert, err := loadCert(opts.ConfDir)
  74. if err != nil {
  75. newCertificate(opts.ConfDir)
  76. cert, err = loadCert(opts.ConfDir)
  77. fatalErr(err)
  78. }
  79. myID := string(certId(cert.Certificate[0]))
  80. infoln("My ID:", myID)
  81. if opts.Debug.Profiler != "" {
  82. go func() {
  83. err := http.ListenAndServe(opts.Debug.Profiler, nil)
  84. if err != nil {
  85. warnln(err)
  86. }
  87. }()
  88. }
  89. // The TLS configuration is used for both the listening socket and outgoing
  90. // connections.
  91. cfg := &tls.Config{
  92. ClientAuth: tls.RequestClientCert,
  93. ServerName: "syncthing",
  94. NextProtos: []string{"bep/1.0"},
  95. InsecureSkipVerify: true,
  96. Certificates: []tls.Certificate{cert},
  97. }
  98. // Load the configuration file, if it exists.
  99. cf, err := os.Open(path.Join(opts.ConfDir, confFileName))
  100. if err != nil {
  101. fatalln("No config file")
  102. config = ini.Config{}
  103. }
  104. config = ini.Parse(cf)
  105. cf.Close()
  106. var dir = config.Get("repository", "dir")
  107. // Create a map of desired node connections based on the configuration file
  108. // directives.
  109. for nodeID, addrs := range config.OptionMap("nodes") {
  110. addrs := strings.Fields(addrs)
  111. nodeAddrs[nodeID] = addrs
  112. }
  113. ensureDir(dir, -1)
  114. m := NewModel(dir)
  115. // Walk the repository and update the local model before establishing any
  116. // connections to other nodes.
  117. infoln("Initial repository scan in progress")
  118. loadIndex(m)
  119. updateLocalModel(m)
  120. // Routine to listen for incoming connections
  121. infoln("Listening for incoming connections")
  122. go listen(myID, opts.Listen, m, cfg)
  123. // Routine to connect out to configured nodes
  124. infoln("Attempting to connect to other nodes")
  125. go connect(myID, opts.Listen, nodeAddrs, m, cfg)
  126. // Routine to pull blocks from other nodes to synchronize the local
  127. // repository. Does not run when we are in read only (publish only) mode.
  128. if !opts.ReadOnly {
  129. infoln("Cleaning out incomplete synchronizations")
  130. CleanTempFiles(dir)
  131. okln("Ready to synchronize")
  132. m.Start()
  133. }
  134. // Periodically scan the repository and update the local model.
  135. // XXX: Should use some fsnotify mechanism.
  136. go func() {
  137. for {
  138. time.Sleep(opts.Advanced.ScanInterval)
  139. updateLocalModel(m)
  140. }
  141. }()
  142. select {}
  143. }
  144. func listen(myID string, addr string, m *Model, cfg *tls.Config) {
  145. l, err := tls.Listen("tcp", addr, cfg)
  146. fatalErr(err)
  147. listen:
  148. for {
  149. conn, err := l.Accept()
  150. if err != nil {
  151. warnln(err)
  152. continue
  153. }
  154. if opts.Debug.TraceNet {
  155. debugln("NET: Connect from", conn.RemoteAddr())
  156. }
  157. tc := conn.(*tls.Conn)
  158. err = tc.Handshake()
  159. if err != nil {
  160. warnln(err)
  161. tc.Close()
  162. continue
  163. }
  164. remoteID := certId(tc.ConnectionState().PeerCertificates[0].Raw)
  165. if remoteID == myID {
  166. warnf("Connect from myself (%s) - should not happen", remoteID)
  167. conn.Close()
  168. continue
  169. }
  170. if m.ConnectedTo(remoteID) {
  171. warnf("Connect from connected node (%s)", remoteID)
  172. }
  173. for nodeID := range nodeAddrs {
  174. if nodeID == remoteID {
  175. m.AddConnection(conn, remoteID)
  176. continue listen
  177. }
  178. }
  179. conn.Close()
  180. }
  181. }
  182. func connect(myID string, addr string, nodeAddrs map[string][]string, m *Model, cfg *tls.Config) {
  183. _, portstr, err := net.SplitHostPort(addr)
  184. fatalErr(err)
  185. port, _ := strconv.Atoi(portstr)
  186. if opts.Discovery.NoLocalDiscovery {
  187. port = -1
  188. } else {
  189. infoln("Sending local discovery announcements")
  190. }
  191. if opts.Discovery.NoExternalDiscovery {
  192. opts.Discovery.ExternalPort = -1
  193. } else {
  194. infoln("Sending external discovery announcements")
  195. }
  196. disc, err := discover.NewDiscoverer(myID, port, opts.Discovery.ExternalPort, opts.Discovery.ExternalServer)
  197. if err != nil {
  198. warnf("No discovery possible (%v)", err)
  199. }
  200. for {
  201. nextNode:
  202. for nodeID, addrs := range nodeAddrs {
  203. if nodeID == myID {
  204. continue
  205. }
  206. if m.ConnectedTo(nodeID) {
  207. continue
  208. }
  209. for _, addr := range addrs {
  210. if addr == "dynamic" {
  211. var ok bool
  212. if disc != nil {
  213. addr, ok = disc.Lookup(nodeID)
  214. }
  215. if !ok {
  216. continue
  217. }
  218. }
  219. if opts.Debug.TraceNet {
  220. debugln("NET: Dial", nodeID, addr)
  221. }
  222. conn, err := tls.Dial("tcp", addr, cfg)
  223. if err != nil {
  224. if opts.Debug.TraceNet {
  225. debugln("NET:", err)
  226. }
  227. continue
  228. }
  229. remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
  230. if remoteID != nodeID {
  231. warnln("Unexpected nodeID", remoteID, "!=", nodeID)
  232. conn.Close()
  233. continue
  234. }
  235. m.AddConnection(conn, remoteID)
  236. continue nextNode
  237. }
  238. }
  239. time.Sleep(opts.Advanced.ConnInterval)
  240. }
  241. }
  242. func updateLocalModel(m *Model) {
  243. files := Walk(m.Dir(), m, !opts.NoSymlinks)
  244. m.ReplaceLocal(files)
  245. saveIndex(m)
  246. }
  247. func saveIndex(m *Model) {
  248. name := fmt.Sprintf("%x.idx", sha1.Sum([]byte(m.Dir())))
  249. fullName := path.Join(opts.ConfDir, name)
  250. idxf, err := os.Create(fullName + ".tmp")
  251. if err != nil {
  252. return
  253. }
  254. protocol.WriteIndex(idxf, m.ProtocolIndex())
  255. idxf.Close()
  256. os.Rename(fullName+".tmp", fullName)
  257. }
  258. func loadIndex(m *Model) {
  259. fname := fmt.Sprintf("%x.idx", sha1.Sum([]byte(m.Dir())))
  260. idxf, err := os.Open(path.Join(opts.ConfDir, fname))
  261. if err != nil {
  262. return
  263. }
  264. defer idxf.Close()
  265. idx, err := protocol.ReadIndex(idxf)
  266. if err != nil {
  267. return
  268. }
  269. m.SeedIndex(idx)
  270. }
  271. func ensureDir(dir string, mode int) {
  272. fi, err := os.Stat(dir)
  273. if os.IsNotExist(err) {
  274. err := os.MkdirAll(dir, 0700)
  275. fatalErr(err)
  276. } else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
  277. err := os.Chmod(dir, os.FileMode(mode))
  278. fatalErr(err)
  279. }
  280. }
  281. func getHomeDir() string {
  282. home := os.Getenv("HOME")
  283. if home == "" {
  284. fatalln("No home directory?")
  285. }
  286. return home
  287. }