dnsname.go 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. // Package dnsname contains string functions for working with DNS names.
  4. package dnsname
  5. import (
  6. "errors"
  7. "fmt"
  8. "strings"
  9. )
  10. const (
  11. // maxLabelLength is the maximum length of a label permitted by RFC 1035.
  12. maxLabelLength = 63
  13. // maxNameLength is the maximum length of a DNS name.
  14. maxNameLength = 253
  15. )
  16. // A FQDN is a fully-qualified DNS name or name suffix.
  17. type FQDN string
  18. func ToFQDN(s string) (FQDN, error) {
  19. if len(s) == 0 || s == "." {
  20. return FQDN("."), nil
  21. }
  22. if s[0] == '.' {
  23. s = s[1:]
  24. }
  25. raw := s
  26. totalLen := len(s)
  27. if s[len(s)-1] == '.' {
  28. s = s[:len(s)-1]
  29. } else {
  30. totalLen += 1 // account for missing dot
  31. }
  32. if totalLen > maxNameLength {
  33. return "", fmt.Errorf("%q is too long to be a DNS name", s)
  34. }
  35. st := 0
  36. for i := 0; i < len(s); i++ {
  37. if s[i] != '.' {
  38. continue
  39. }
  40. label := s[st:i]
  41. // You might be tempted to do further validation of the
  42. // contents of labels here, based on the hostname rules in RFC
  43. // 1123. However, DNS labels are not always subject to
  44. // hostname rules. In general, they can contain any non-zero
  45. // byte sequence, even though in practice a more restricted
  46. // set is used.
  47. //
  48. // See https://github.com/tailscale/tailscale/issues/2024 for more.
  49. if len(label) == 0 || len(label) > maxLabelLength {
  50. return "", fmt.Errorf("%q is not a valid DNS label", label)
  51. }
  52. st = i + 1
  53. }
  54. if raw[len(raw)-1] != '.' {
  55. raw = raw + "."
  56. }
  57. return FQDN(raw), nil
  58. }
  59. // WithTrailingDot returns f as a string, with a trailing dot.
  60. func (f FQDN) WithTrailingDot() string {
  61. return string(f)
  62. }
  63. // WithoutTrailingDot returns f as a string, with the trailing dot
  64. // removed.
  65. func (f FQDN) WithoutTrailingDot() string {
  66. return string(f[:len(f)-1])
  67. }
  68. func (f FQDN) NumLabels() int {
  69. if f == "." {
  70. return 0
  71. }
  72. return strings.Count(f.WithTrailingDot(), ".")
  73. }
  74. func (f FQDN) Contains(other FQDN) bool {
  75. if f == other {
  76. return true
  77. }
  78. cmp := f.WithTrailingDot()
  79. if cmp != "." {
  80. cmp = "." + cmp
  81. }
  82. return strings.HasSuffix(other.WithTrailingDot(), cmp)
  83. }
  84. // ValidLabel reports whether label is a valid DNS label.
  85. func ValidLabel(label string) error {
  86. if len(label) == 0 {
  87. return errors.New("empty DNS label")
  88. }
  89. if len(label) > maxLabelLength {
  90. return fmt.Errorf("%q is too long, max length is %d bytes", label, maxLabelLength)
  91. }
  92. if !isalphanum(label[0]) {
  93. return fmt.Errorf("%q is not a valid DNS label: must start with a letter or number", label)
  94. }
  95. if !isalphanum(label[len(label)-1]) {
  96. return fmt.Errorf("%q is not a valid DNS label: must end with a letter or number", label)
  97. }
  98. if len(label) < 2 {
  99. return nil
  100. }
  101. for i := 1; i < len(label)-1; i++ {
  102. if !isdnschar(label[i]) {
  103. return fmt.Errorf("%q is not a valid DNS label: contains invalid character %q", label, label[i])
  104. }
  105. }
  106. return nil
  107. }
  108. // SanitizeLabel takes a string intended to be a DNS name label
  109. // and turns it into a valid name label according to RFC 1035.
  110. func SanitizeLabel(label string) string {
  111. var sb strings.Builder // TODO: don't allocate in common case where label is already fine
  112. start, end := 0, len(label)
  113. // This is technically stricter than necessary as some characters may be dropped,
  114. // but labels have no business being anywhere near this long in any case.
  115. if end > maxLabelLength {
  116. end = maxLabelLength
  117. }
  118. // A label must start with a letter or number...
  119. for ; start < end; start++ {
  120. if isalphanum(label[start]) {
  121. break
  122. }
  123. }
  124. // ...and end with a letter or number.
  125. for ; start < end; end-- {
  126. // This is safe because (start < end) implies (end >= 1).
  127. if isalphanum(label[end-1]) {
  128. break
  129. }
  130. }
  131. for i := start; i < end; i++ {
  132. // Consume a separator only if we are not at a boundary:
  133. // then we can turn it into a hyphen without breaking the rules.
  134. boundary := (i == start) || (i == end-1)
  135. if !boundary && separators[label[i]] {
  136. sb.WriteByte('-')
  137. } else if isdnschar(label[i]) {
  138. sb.WriteByte(tolower(label[i]))
  139. }
  140. }
  141. return sb.String()
  142. }
  143. // HasSuffix reports whether the provided name ends with the
  144. // component(s) in suffix, ignoring any trailing or leading dots.
  145. //
  146. // If suffix is the empty string, HasSuffix always reports false.
  147. func HasSuffix(name, suffix string) bool {
  148. name = strings.TrimSuffix(name, ".")
  149. suffix = strings.TrimSuffix(suffix, ".")
  150. suffix = strings.TrimPrefix(suffix, ".")
  151. nameBase := strings.TrimSuffix(name, suffix)
  152. return len(nameBase) < len(name) && strings.HasSuffix(nameBase, ".")
  153. }
  154. // TrimSuffix trims any trailing dots from a name and removes the
  155. // suffix ending if present. The name will never be returned with
  156. // a trailing dot, even after trimming.
  157. func TrimSuffix(name, suffix string) string {
  158. if HasSuffix(name, suffix) {
  159. name = strings.TrimSuffix(name, ".")
  160. suffix = strings.Trim(suffix, ".")
  161. name = strings.TrimSuffix(name, suffix)
  162. }
  163. return strings.TrimSuffix(name, ".")
  164. }
  165. // TrimCommonSuffixes returns hostname with some common suffixes removed.
  166. func TrimCommonSuffixes(hostname string) string {
  167. hostname = strings.TrimSuffix(hostname, ".local")
  168. hostname = strings.TrimSuffix(hostname, ".localdomain")
  169. hostname = strings.TrimSuffix(hostname, ".lan")
  170. return hostname
  171. }
  172. // SanitizeHostname turns hostname into a valid name label according
  173. // to RFC 1035.
  174. func SanitizeHostname(hostname string) string {
  175. hostname = TrimCommonSuffixes(hostname)
  176. return SanitizeLabel(hostname)
  177. }
  178. // NumLabels returns the number of DNS labels in hostname.
  179. // If hostname is empty or the top-level name ".", returns 0.
  180. func NumLabels(hostname string) int {
  181. if hostname == "" || hostname == "." {
  182. return 0
  183. }
  184. return strings.Count(hostname, ".")
  185. }
  186. // FirstLabel returns the first DNS label of hostname.
  187. func FirstLabel(hostname string) string {
  188. first, _, _ := strings.Cut(hostname, ".")
  189. return first
  190. }
  191. // ValidHostname checks if a string is a valid hostname.
  192. func ValidHostname(hostname string) error {
  193. fqdn, err := ToFQDN(hostname)
  194. if err != nil {
  195. return err
  196. }
  197. for _, label := range strings.Split(fqdn.WithoutTrailingDot(), ".") {
  198. if err := ValidLabel(label); err != nil {
  199. return err
  200. }
  201. }
  202. return nil
  203. }
  204. var separators = map[byte]bool{
  205. ' ': true,
  206. '.': true,
  207. '@': true,
  208. '_': true,
  209. }
  210. func islower(c byte) bool {
  211. return 'a' <= c && c <= 'z'
  212. }
  213. func isupper(c byte) bool {
  214. return 'A' <= c && c <= 'Z'
  215. }
  216. func isalpha(c byte) bool {
  217. return islower(c) || isupper(c)
  218. }
  219. func isalphanum(c byte) bool {
  220. return isalpha(c) || ('0' <= c && c <= '9')
  221. }
  222. func isdnschar(c byte) bool {
  223. return isalphanum(c) || c == '-'
  224. }
  225. func tolower(c byte) byte {
  226. if isupper(c) {
  227. return c + 'a' - 'A'
  228. } else {
  229. return c
  230. }
  231. }