pmp.go 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. // Copyright (C) 2016 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package pmp
  7. import (
  8. "context"
  9. "fmt"
  10. "net"
  11. "strings"
  12. "time"
  13. "github.com/jackpal/gateway"
  14. "github.com/jackpal/go-nat-pmp"
  15. "github.com/syncthing/syncthing/lib/nat"
  16. "github.com/syncthing/syncthing/lib/util"
  17. )
  18. func init() {
  19. nat.Register(Discover)
  20. }
  21. func Discover(ctx context.Context, renewal, timeout time.Duration) []nat.Device {
  22. var ip net.IP
  23. err := util.CallWithContext(ctx, func() error {
  24. var err error
  25. ip, err = gateway.DiscoverGateway()
  26. return err
  27. })
  28. if err != nil {
  29. l.Debugln("Failed to discover gateway", err)
  30. return nil
  31. }
  32. if ip == nil || ip.IsUnspecified() {
  33. return nil
  34. }
  35. l.Debugln("Discovered gateway at", ip)
  36. c := natpmp.NewClientWithTimeout(ip, timeout)
  37. // Try contacting the gateway, if it does not respond, assume it does not
  38. // speak NAT-PMP.
  39. _, err = c.GetExternalAddress()
  40. if err != nil && strings.Contains(err.Error(), "Timed out") {
  41. l.Debugln("Timeout trying to get external address, assume no NAT-PMP available")
  42. return nil
  43. }
  44. var localIP net.IP
  45. // Port comes from the natpmp package
  46. timeoutCtx, cancel := context.WithTimeout(ctx, timeout)
  47. defer cancel()
  48. conn, err := (&net.Dialer{}).DialContext(timeoutCtx, "udp", net.JoinHostPort(ip.String(), "5351"))
  49. if err == nil {
  50. conn.Close()
  51. localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String())
  52. if err == nil {
  53. localIP = net.ParseIP(localIPAddress)
  54. } else {
  55. l.Debugln("Failed to lookup local IP", err)
  56. }
  57. }
  58. return []nat.Device{&wrapper{
  59. renewal: renewal,
  60. localIP: localIP,
  61. gatewayIP: ip,
  62. client: c,
  63. }}
  64. }
  65. type wrapper struct {
  66. renewal time.Duration
  67. localIP net.IP
  68. gatewayIP net.IP
  69. client *natpmp.Client
  70. }
  71. func (w *wrapper) ID() string {
  72. return fmt.Sprintf("NAT-PMP@%s", w.gatewayIP.String())
  73. }
  74. func (w *wrapper) GetLocalIPAddress() net.IP {
  75. return w.localIP
  76. }
  77. func (w *wrapper) AddPortMapping(ctx context.Context, protocol nat.Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error) {
  78. // NAT-PMP says that if duration is 0, the mapping is actually removed
  79. // Swap the zero with the renewal value, which should make the lease for the
  80. // exact amount of time between the calls.
  81. if duration == 0 {
  82. duration = w.renewal
  83. }
  84. var result *natpmp.AddPortMappingResult
  85. err := util.CallWithContext(ctx, func() error {
  86. var err error
  87. result, err = w.client.AddPortMapping(strings.ToLower(string(protocol)), internalPort, externalPort, int(duration/time.Second))
  88. return err
  89. })
  90. port := 0
  91. if result != nil {
  92. port = int(result.MappedExternalPort)
  93. }
  94. return port, err
  95. }
  96. func (w *wrapper) GetExternalIPAddress(ctx context.Context) (net.IP, error) {
  97. var result *natpmp.GetExternalAddressResult
  98. err := util.CallWithContext(ctx, func() error {
  99. var err error
  100. result, err = w.client.GetExternalAddress()
  101. return err
  102. })
  103. ip := net.IPv4zero
  104. if result != nil {
  105. ip = net.IPv4(
  106. result.ExternalIPAddress[0],
  107. result.ExternalIPAddress[1],
  108. result.ExternalIPAddress[2],
  109. result.ExternalIPAddress[3],
  110. )
  111. }
  112. return ip, err
  113. }