probe.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package proxy
  2. import (
  3. "bufio"
  4. "context"
  5. "errors"
  6. "fmt"
  7. "io"
  8. "net"
  9. "strings"
  10. "time"
  11. "vocat/internal/i18n"
  12. )
  13. type ProbeResult struct {
  14. Reachable bool `json:"reachable"`
  15. HandshakeOK bool `json:"handshake_ok"`
  16. UDPAssociateOK bool `json:"udp_associate_ok"`
  17. AuthMethod string `json:"auth_method,omitempty"`
  18. RelayAddr string `json:"relay_addr,omitempty"`
  19. Diagnosis string `json:"diagnosis,omitempty"`
  20. Hint string `json:"hint,omitempty"`
  21. }
  22. func ProbeSOCKS5(
  23. ctx context.Context,
  24. address string,
  25. username string,
  26. password string,
  27. timeout time.Duration,
  28. ) (ProbeResult, error) {
  29. address = strings.TrimSpace(address)
  30. if _, _, err := net.SplitHostPort(address); err != nil {
  31. return ProbeResult{}, fmt.Errorf("proxy: upstream address must be host:port: %w", err)
  32. }
  33. if timeout <= 0 {
  34. timeout = 8 * time.Second
  35. }
  36. probeContext, cancel := context.WithTimeout(ctx, timeout)
  37. defer cancel()
  38. connection, err := (&net.Dialer{Timeout: timeout}).DialContext(probeContext, "tcp", address)
  39. if err != nil {
  40. return ProbeResult{
  41. Diagnosis: "tcp_unreachable",
  42. Hint: i18n.T("检查地址、端口、防火墙与上游代理监听状态。"),
  43. }, err
  44. }
  45. defer connection.Close()
  46. result := ProbeResult{Reachable: true}
  47. _ = connection.SetDeadline(time.Now().Add(timeout))
  48. methods := []byte{0}
  49. if username != "" {
  50. methods = append(methods, 2)
  51. }
  52. greeting := append([]byte{5, byte(len(methods))}, methods...)
  53. if _, err := connection.Write(greeting); err != nil {
  54. return result, err
  55. }
  56. methodResponse := make([]byte, 2)
  57. if _, err := io.ReadFull(connection, methodResponse); err != nil {
  58. return result, err
  59. }
  60. if methodResponse[0] != 5 || methodResponse[1] == 0xff {
  61. result.Diagnosis = "no_acceptable_auth"
  62. return result, errors.New("proxy: upstream rejected all SOCKS5 authentication methods")
  63. }
  64. switch methodResponse[1] {
  65. case 0:
  66. result.AuthMethod = "none"
  67. case 2:
  68. result.AuthMethod = "username_password"
  69. if username == "" || len(username) > 255 || len(password) > 255 {
  70. return result, errors.New("proxy: upstream requires username/password authentication")
  71. }
  72. authRequest := []byte{1, byte(len(username))}
  73. authRequest = append(authRequest, []byte(username)...)
  74. authRequest = append(authRequest, byte(len(password)))
  75. authRequest = append(authRequest, []byte(password)...)
  76. if _, err := connection.Write(authRequest); err != nil {
  77. return result, err
  78. }
  79. authResponse := make([]byte, 2)
  80. if _, err := io.ReadFull(connection, authResponse); err != nil {
  81. return result, err
  82. }
  83. if authResponse[0] != 1 || authResponse[1] != 0 {
  84. result.Diagnosis = "authentication_failed"
  85. return result, errors.New("proxy: upstream username/password authentication failed")
  86. }
  87. default:
  88. result.AuthMethod = fmt.Sprintf("method_%d", methodResponse[1])
  89. return result, errors.New("proxy: upstream selected an unsupported authentication method")
  90. }
  91. result.HandshakeOK = true
  92. if _, err := connection.Write([]byte{5, 3, 0, 1, 0, 0, 0, 0, 0, 0}); err != nil {
  93. return result, err
  94. }
  95. reader := bufio.NewReader(connection)
  96. header := make([]byte, 4)
  97. if _, err := io.ReadFull(reader, header); err != nil {
  98. return result, err
  99. }
  100. if header[0] != 5 {
  101. return result, errors.New("proxy: invalid UDP ASSOCIATE response version")
  102. }
  103. if header[1] != 0 {
  104. result.Diagnosis = "udp_associate_rejected"
  105. result.Hint = i18n.T("该代理不能承载 ePDG 所需的 UDP;启用上游 SOCKS5 UDP 转发后重试。")
  106. return result, fmt.Errorf("proxy: upstream rejected UDP ASSOCIATE with code %d", header[1])
  107. }
  108. host, err := readSOCKSAddress(reader, header[3])
  109. if err != nil {
  110. return result, err
  111. }
  112. portBytes := make([]byte, 2)
  113. if _, err := io.ReadFull(reader, portBytes); err != nil {
  114. return result, err
  115. }
  116. port := int(portBytes[0])<<8 | int(portBytes[1])
  117. result.UDPAssociateOK = true
  118. result.RelayAddr = net.JoinHostPort(host, fmt.Sprintf("%d", port))
  119. result.Diagnosis = "ready"
  120. result.Hint = i18n.T("TCP 握手、认证和 UDP ASSOCIATE 均通过。")
  121. return result, nil
  122. }
  123. func readSOCKSAddress(reader io.Reader, addressType byte) (string, error) {
  124. switch addressType {
  125. case 1:
  126. value := make([]byte, net.IPv4len)
  127. if _, err := io.ReadFull(reader, value); err != nil {
  128. return "", err
  129. }
  130. return net.IP(value).String(), nil
  131. case 3:
  132. var length [1]byte
  133. if _, err := io.ReadFull(reader, length[:]); err != nil {
  134. return "", err
  135. }
  136. if length[0] == 0 {
  137. return "", errors.New("empty SOCKS5 domain")
  138. }
  139. value := make([]byte, int(length[0]))
  140. if _, err := io.ReadFull(reader, value); err != nil {
  141. return "", err
  142. }
  143. return string(value), nil
  144. case 4:
  145. value := make([]byte, net.IPv6len)
  146. if _, err := io.ReadFull(reader, value); err != nil {
  147. return "", err
  148. }
  149. return net.IP(value).String(), nil
  150. default:
  151. return "", errors.New("unsupported SOCKS5 address type")
  152. }
  153. }