geoip.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  1. /*
  2. This code is for loading database data that maps ip addresses to countries
  3. for collecting and presenting statistics on snowflake use that might alert us
  4. to censorship events.
  5. The functions here are heavily based off of how tor maintains and searches their
  6. geoip database
  7. The tables used for geoip data must be structured as follows:
  8. Recognized line format for IPv4 is:
  9. INTIPLOW,INTIPHIGH,CC
  10. where INTIPLOW and INTIPHIGH are IPv4 addresses encoded as big-endian 4-byte unsigned
  11. integers, and CC is a country code.
  12. Note that the IPv4 line format
  13. "INTIPLOW","INTIPHIGH","CC","CC3","COUNTRY NAME"
  14. is not currently supported.
  15. Recognized line format for IPv6 is:
  16. IPV6LOW,IPV6HIGH,CC
  17. where IPV6LOW and IPV6HIGH are IPv6 addresses and CC is a country code.
  18. It also recognizes, and skips over, blank lines and lines that start
  19. with '#' (comments).
  20. */
  21. package main
  22. import (
  23. "bufio"
  24. "bytes"
  25. "crypto/sha1"
  26. "encoding/hex"
  27. "fmt"
  28. "io"
  29. "log"
  30. "net"
  31. "os"
  32. "sort"
  33. "strconv"
  34. "strings"
  35. "sync"
  36. )
  37. type GeoIPTable interface {
  38. parseEntry(string) (*GeoIPEntry, error)
  39. Len() int
  40. Append(GeoIPEntry)
  41. ElementAt(int) GeoIPEntry
  42. Lock()
  43. Unlock()
  44. }
  45. type GeoIPEntry struct {
  46. ipLow net.IP
  47. ipHigh net.IP
  48. country string
  49. }
  50. type GeoIPv4Table struct {
  51. table []GeoIPEntry
  52. lock sync.Mutex // synchronization for geoip table accesses and reloads
  53. }
  54. type GeoIPv6Table struct {
  55. table []GeoIPEntry
  56. lock sync.Mutex // synchronization for geoip table accesses and reloads
  57. }
  58. func (table *GeoIPv4Table) Len() int { return len(table.table) }
  59. func (table *GeoIPv6Table) Len() int { return len(table.table) }
  60. func (table *GeoIPv4Table) Append(entry GeoIPEntry) {
  61. (*table).table = append(table.table, entry)
  62. }
  63. func (table *GeoIPv6Table) Append(entry GeoIPEntry) {
  64. (*table).table = append(table.table, entry)
  65. }
  66. func (table *GeoIPv4Table) ElementAt(i int) GeoIPEntry { return table.table[i] }
  67. func (table *GeoIPv6Table) ElementAt(i int) GeoIPEntry { return table.table[i] }
  68. func (table *GeoIPv4Table) Lock() { (*table).lock.Lock() }
  69. func (table *GeoIPv6Table) Lock() { (*table).lock.Lock() }
  70. func (table *GeoIPv4Table) Unlock() { (*table).lock.Unlock() }
  71. func (table *GeoIPv6Table) Unlock() { (*table).lock.Unlock() }
  72. // Convert a geoip IP address represented as a big-endian unsigned integer to net.IP
  73. func geoipStringToIP(ipStr string) (net.IP, error) {
  74. ip, err := strconv.ParseUint(ipStr, 10, 32)
  75. if err != nil {
  76. return net.IPv4(0, 0, 0, 0), fmt.Errorf("error parsing IP %s", ipStr)
  77. }
  78. var bytes [4]byte
  79. bytes[0] = byte(ip & 0xFF)
  80. bytes[1] = byte((ip >> 8) & 0xFF)
  81. bytes[2] = byte((ip >> 16) & 0xFF)
  82. bytes[3] = byte((ip >> 24) & 0xFF)
  83. return net.IPv4(bytes[3], bytes[2], bytes[1], bytes[0]), nil
  84. }
  85. //Parses a line in the provided geoip file that corresponds
  86. //to an address range and a two character country code
  87. func (table *GeoIPv4Table) parseEntry(candidate string) (*GeoIPEntry, error) {
  88. if candidate[0] == '#' {
  89. return nil, nil
  90. }
  91. parsedCandidate := strings.Split(candidate, ",")
  92. if len(parsedCandidate) != 3 {
  93. return nil, fmt.Errorf("provided geoip file is incorrectly formatted. Could not parse line:\n%s", parsedCandidate)
  94. }
  95. low, err := geoipStringToIP(parsedCandidate[0])
  96. if err != nil {
  97. return nil, err
  98. }
  99. high, err := geoipStringToIP(parsedCandidate[1])
  100. if err != nil {
  101. return nil, err
  102. }
  103. geoipEntry := &GeoIPEntry{
  104. ipLow: low,
  105. ipHigh: high,
  106. country: parsedCandidate[2],
  107. }
  108. return geoipEntry, nil
  109. }
  110. //Parses a line in the provided geoip file that corresponds
  111. //to an address range and a two character country code
  112. func (table *GeoIPv6Table) parseEntry(candidate string) (*GeoIPEntry, error) {
  113. if candidate[0] == '#' {
  114. return nil, nil
  115. }
  116. parsedCandidate := strings.Split(candidate, ",")
  117. if len(parsedCandidate) != 3 {
  118. return nil, fmt.Errorf("")
  119. }
  120. low := net.ParseIP(parsedCandidate[0])
  121. if low == nil {
  122. return nil, fmt.Errorf("")
  123. }
  124. high := net.ParseIP(parsedCandidate[1])
  125. if high == nil {
  126. return nil, fmt.Errorf("")
  127. }
  128. geoipEntry := &GeoIPEntry{
  129. ipLow: low,
  130. ipHigh: high,
  131. country: parsedCandidate[2],
  132. }
  133. return geoipEntry, nil
  134. }
  135. //Loads provided geoip file into our tables
  136. //Entries are stored in a table
  137. func GeoIPLoadFile(table GeoIPTable, pathname string) error {
  138. //open file
  139. geoipFile, err := os.Open(pathname)
  140. if err != nil {
  141. return err
  142. }
  143. defer geoipFile.Close()
  144. hash := sha1.New()
  145. table.Lock()
  146. defer table.Unlock()
  147. hashedFile := io.TeeReader(geoipFile, hash)
  148. //read in strings and call parse function
  149. scanner := bufio.NewScanner(hashedFile)
  150. for scanner.Scan() {
  151. entry, err := table.parseEntry(scanner.Text())
  152. if err != nil {
  153. return fmt.Errorf("provided geoip file is incorrectly formatted. Line is: %+q", scanner.Text())
  154. }
  155. if entry != nil {
  156. table.Append(*entry)
  157. }
  158. }
  159. if err := scanner.Err(); err != nil {
  160. return err
  161. }
  162. sha1Hash := hex.EncodeToString(hash.Sum(nil))
  163. log.Println("Using geoip file ", pathname, " with checksum", sha1Hash)
  164. log.Println("Loaded ", table.Len(), " entries into table")
  165. return nil
  166. }
  167. //Returns the country location of an IPv4 or IPv6 address, and a boolean value
  168. //that indicates whether the IP address was present in the geoip database
  169. func GetCountryByAddr(table GeoIPTable, ip net.IP) (string, bool) {
  170. table.Lock()
  171. defer table.Unlock()
  172. //look IP up in database
  173. index := sort.Search(table.Len(), func(i int) bool {
  174. entry := table.ElementAt(i)
  175. return (bytes.Compare(ip.To16(), entry.ipHigh.To16()) <= 0)
  176. })
  177. if index == table.Len() {
  178. return "", false
  179. }
  180. // check to see if addr is in the range specified by the returned index
  181. // search on IPs in invalid ranges (e.g., 127.0.0.0/8) will return the
  182. //country code of the next highest range
  183. entry := table.ElementAt(index)
  184. if !(bytes.Compare(ip.To16(), entry.ipLow.To16()) >= 0 &&
  185. bytes.Compare(ip.To16(), entry.ipHigh.To16()) <= 0) {
  186. return "", false
  187. }
  188. return table.ElementAt(index).country, true
  189. }