geodata.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. // Package geodata reads Xray's geosite/geoip .dat databases so the panel can
  2. // browse their categories instead of asking the user to type category names
  3. // from memory.
  4. //
  5. // The databases are protobuf, but decoding them into Go structs is what makes
  6. // them expensive: a 10 MB geosite.dat holds well over a million domains, and
  7. // materialising all of them costs hundreds of megabytes on a panel that often
  8. // runs with 512 MB of RAM. The readers therefore walk the wire format directly
  9. // and allocate only what the caller asked for — category counts for the index,
  10. // one page of values for the browser.
  11. package geodata
  12. import (
  13. "errors"
  14. "os"
  15. "path/filepath"
  16. "strings"
  17. "sync"
  18. )
  19. // MaxFileSize is the largest database the panel will parse. Community rule
  20. // sets are far bigger than the official ones — russia-v2ray-rules-dat ships a
  21. // 70 MB geosite — so the ceiling is set well above them; reading is streaming
  22. // and serialised, so a scan costs about the file's own size once, not per
  23. // request. The limit exists only to keep a stray huge file in the asset folder
  24. // from taking the panel down with it.
  25. const MaxFileSize int64 = 256 << 20
  26. // MaxPageSize caps how many rows a single page may carry, independent of what
  27. // the caller asks for.
  28. const MaxPageSize = 500
  29. var (
  30. // ErrFileTooLarge reports a database above MaxFileSize.
  31. ErrFileTooLarge = errors.New("geodata file is too large to browse")
  32. // ErrInvalidName reports a file name that does not resolve to a .dat file
  33. // directly inside the asset directory.
  34. ErrInvalidName = errors.New("invalid geodata file name")
  35. // ErrUnknownCategory reports a category code missing from the database.
  36. ErrUnknownCategory = errors.New("unknown geodata category")
  37. )
  38. // GeoKind tells apart the two database layouts Xray ships.
  39. type GeoKind string
  40. const (
  41. KindSite GeoKind = "site"
  42. KindIP GeoKind = "ip"
  43. )
  44. // GeoFile describes one .dat database found in the asset directory.
  45. type GeoFile struct {
  46. Name string `json:"name" example:"geosite.dat"`
  47. Kind GeoKind `json:"kind" example:"site"`
  48. Size int64 `json:"size" example:"1467392"`
  49. ModifiedAt int64 `json:"modifiedAt" example:"1769558400000"`
  50. Categories int `json:"categories" example:"1043"`
  51. Error string `json:"error,omitempty" example:""`
  52. }
  53. // GeoCategory is one code inside a database, such as geosite's "google".
  54. type GeoCategory struct {
  55. Code string `json:"code" example:"google"`
  56. Entries int `json:"entries" example:"1284"`
  57. Attributes []string `json:"attributes" example:"[\"ads\",\"cn\"]"`
  58. }
  59. // GeoEntry is a single rule inside a category: a domain rule for geosite
  60. // databases, a CIDR for geoip ones.
  61. type GeoEntry struct {
  62. Kind string `json:"kind" example:"domain"`
  63. Value string `json:"value" example:"google.com"`
  64. }
  65. // GeoCategoryPage is one page of categories plus the unpaged total.
  66. type GeoCategoryPage struct {
  67. Total int `json:"total" example:"1043"`
  68. Items []GeoCategory `json:"items"`
  69. }
  70. // GeoEntryPage is one page of category entries plus the unpaged total.
  71. type GeoEntryPage struct {
  72. Total int `json:"total" example:"1284"`
  73. Items []GeoEntry `json:"items"`
  74. }
  75. type fileKey struct {
  76. name string
  77. size int64
  78. modTime int64
  79. }
  80. type index struct {
  81. kind GeoKind
  82. categories []GeoCategory
  83. byCode map[string]GeoCategory
  84. spans map[string][]byteSpan
  85. err error
  86. }
  87. // Store reads databases from one asset directory. Only the category index is
  88. // cached, for as long as the file on disk is unchanged; entry pages are scanned
  89. // out of the file on demand, which keeps a browsing session's memory close to
  90. // the size of the page being shown rather than the size of the database.
  91. //
  92. // Scans are serialised on purpose. Reading a database allocates on the order of
  93. // its own size, so letting a page's parallel requests — or a scripted caller —
  94. // scan several databases at once is what turns a browsable panel into an
  95. // out-of-memory kill on a small VPS.
  96. type Store struct {
  97. dir string
  98. mu sync.Mutex
  99. indexes map[fileKey]*index
  100. scan sync.Mutex
  101. // hot holds the records of the category being paged through, so a browsing
  102. // session reads them once instead of once per page. Only one category is
  103. // kept: paging is the repeated operation, switching categories is not.
  104. hot hotRecord
  105. }
  106. type hotRecord struct {
  107. key fileKey
  108. code string
  109. records [][]byte
  110. }
  111. // NewStore returns a Store reading databases from dir.
  112. func NewStore(dir string) *Store {
  113. return &Store{dir: dir, indexes: make(map[fileKey]*index)}
  114. }
  115. // ListFiles reports every .dat database in the asset directory. A database that
  116. // cannot be parsed is still listed, with the reason in GeoFile.Error, so the panel
  117. // can show a broken download instead of hiding it.
  118. func (s *Store) ListFiles() ([]GeoFile, error) {
  119. dirEntries, err := os.ReadDir(s.dir)
  120. if err != nil {
  121. return nil, err
  122. }
  123. files := make([]GeoFile, 0, len(dirEntries))
  124. for _, dirEntry := range dirEntries {
  125. if dirEntry.IsDir() || !strings.HasSuffix(strings.ToLower(dirEntry.Name()), ".dat") {
  126. continue
  127. }
  128. info, err := dirEntry.Info()
  129. if err != nil {
  130. continue
  131. }
  132. file := GeoFile{
  133. Name: dirEntry.Name(),
  134. Size: info.Size(),
  135. ModifiedAt: info.ModTime().UnixMilli(),
  136. }
  137. idx, err := s.index(dirEntry.Name())
  138. if err != nil {
  139. file.Error = err.Error()
  140. } else {
  141. file.Kind = idx.kind
  142. file.Categories = len(idx.categories)
  143. }
  144. files = append(files, file)
  145. }
  146. return files, nil
  147. }
  148. // resolve validates a client-supplied file name and stats it through an
  149. // os.Root, so a symlink planted in the asset folder cannot be used to read a
  150. // file from elsewhere on disk.
  151. func (s *Store) resolve(name string) (os.FileInfo, error) {
  152. if name == "" || name != filepath.Base(name) || !strings.HasSuffix(strings.ToLower(name), ".dat") {
  153. return nil, ErrInvalidName
  154. }
  155. root, err := os.OpenRoot(s.dir)
  156. if err != nil {
  157. return nil, err
  158. }
  159. defer root.Close()
  160. info, err := root.Stat(name)
  161. if err != nil {
  162. return nil, err
  163. }
  164. if !info.Mode().IsRegular() {
  165. return nil, ErrInvalidName
  166. }
  167. if info.Size() > MaxFileSize {
  168. return nil, ErrFileTooLarge
  169. }
  170. return info, nil
  171. }
  172. func (s *Store) index(name string) (*index, error) {
  173. info, err := s.resolve(name)
  174. if err != nil {
  175. return nil, err
  176. }
  177. key := fileKey{name: name, size: info.Size(), modTime: info.ModTime().UnixNano()}
  178. if cached, ok := s.cachedIndex(key); ok {
  179. return cached, cached.err
  180. }
  181. s.scan.Lock()
  182. defer s.scan.Unlock()
  183. // Another request may have built this index while this one waited.
  184. if cached, ok := s.cachedIndex(key); ok {
  185. return cached, cached.err
  186. }
  187. idx := buildIndex(s.dir, name)
  188. if idx.err != nil && !isPermanent(idx.err) {
  189. // A transient read failure (out of memory on a large file, too many open
  190. // files) must not latch: the file is fine and the next request should
  191. // try again rather than see it greyed out until it changes on disk.
  192. return nil, idx.err
  193. }
  194. s.mu.Lock()
  195. s.indexes[key] = idx
  196. s.dropStaleIndexesLocked(name, key)
  197. s.mu.Unlock()
  198. return idx, idx.err
  199. }
  200. // isPermanent reports whether an error will repeat for the same bytes, and is
  201. // therefore worth caching instead of re-deriving on every request.
  202. func isPermanent(err error) bool {
  203. return errors.Is(err, ErrUnrecognized) || errors.Is(err, ErrInvalidName) || errors.Is(err, ErrFileTooLarge)
  204. }
  205. func (s *Store) cachedIndex(key fileKey) (*index, bool) {
  206. s.mu.Lock()
  207. defer s.mu.Unlock()
  208. cached, ok := s.indexes[key]
  209. return cached, ok
  210. }
  211. // buildIndex never fails outright: a database that cannot be read is cached as
  212. // a failed index, so a broken download is reported without being re-parsed on
  213. // every request.
  214. func buildIndex(dir, name string) *index {
  215. data, err := readDatabase(dir, name)
  216. if err != nil {
  217. return &index{err: err}
  218. }
  219. kind, scan, err := detectKind(data, name)
  220. if err != nil {
  221. return &index{err: err}
  222. }
  223. idx := &index{
  224. kind: kind,
  225. categories: scan.categories,
  226. byCode: make(map[string]GeoCategory, len(scan.categories)),
  227. spans: scan.spans,
  228. }
  229. for _, category := range scan.categories {
  230. idx.byCode[category.Code] = category
  231. }
  232. return idx
  233. }
  234. func (s *Store) dropStaleIndexesLocked(name string, keep fileKey) {
  235. for key := range s.indexes {
  236. if key.name == name && key != keep {
  237. delete(s.indexes, key)
  238. }
  239. }
  240. }