github.com/SmartMeshFoundation/Spectrum@v0.0.0-20220621030607-452a266fee1e/p2p/nat/nat.go (about) 1 // Copyright 2015 The Spectrum Authors 2 // This file is part of the Spectrum library. 3 // 4 // The Spectrum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The Spectrum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the Spectrum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package nat provides access to common network port mapping protocols. 18 package nat 19 20 import ( 21 "errors" 22 "fmt" 23 "net" 24 "strings" 25 "sync" 26 "time" 27 28 "github.com/SmartMeshFoundation/Spectrum/log" 29 "github.com/jackpal/go-nat-pmp" 30 ) 31 32 // An implementation of nat.Interface can map local ports to ports 33 // accessible from the Internet. 34 type Interface interface { 35 // These methods manage a mapping between a port on the local 36 // machine to a port that can be connected to from the internet. 37 // 38 // protocol is "UDP" or "TCP". Some implementations allow setting 39 // a display name for the mapping. The mapping may be removed by 40 // the gateway when its lifetime ends. 41 AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error 42 DeleteMapping(protocol string, extport, intport int) error 43 44 // This method should return the external (Internet-facing) 45 // address of the gateway device. 46 ExternalIP() (net.IP, error) 47 48 // Should return name of the method. This is used for logging. 49 String() string 50 } 51 52 // Parse parses a NAT interface description. 53 // The following formats are currently accepted. 54 // Note that mechanism names are not case-sensitive. 55 // 56 // "" or "none" return nil 57 // "extip:77.12.33.4" will assume the local machine is reachable on the given IP 58 // "any" uses the first auto-detected mechanism 59 // "upnp" uses the Universal Plug and Play protocol 60 // "pmp" uses NAT-PMP with an auto-detected gateway address 61 // "pmp:192.168.0.1" uses NAT-PMP with the given gateway address 62 func Parse(spec string) (Interface, error) { 63 var ( 64 parts = strings.SplitN(spec, ":", 2) 65 mech = strings.ToLower(parts[0]) 66 ip net.IP 67 ) 68 if len(parts) > 1 { 69 ip = net.ParseIP(parts[1]) 70 if ip == nil { 71 return nil, errors.New("invalid IP address") 72 } 73 } 74 switch mech { 75 case "", "none", "off": 76 return nil, nil 77 case "any", "auto", "on": 78 return Any(), nil 79 case "extip", "ip": 80 if ip == nil { 81 return nil, errors.New("missing IP address") 82 } 83 return ExtIP(ip), nil 84 case "upnp": 85 return UPnP(), nil 86 case "pmp", "natpmp", "nat-pmp": 87 return PMP(ip), nil 88 default: 89 return nil, fmt.Errorf("unknown mechanism %q", parts[0]) 90 } 91 } 92 93 const ( 94 mapTimeout = 20 * time.Minute 95 mapUpdateInterval = 15 * time.Minute 96 ) 97 98 // Map adds a port mapping on m and keeps it alive until c is closed. 99 // This function is typically invoked in its own goroutine. 100 func Map(m Interface, c chan struct{}, protocol string, extport, intport int, name string) { 101 log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m) 102 refresh := time.NewTimer(mapUpdateInterval) 103 defer func() { 104 refresh.Stop() 105 log.Debug("Deleting port mapping") 106 m.DeleteMapping(protocol, extport, intport) 107 }() 108 if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil { 109 log.Debug("Couldn't add port mapping", "err", err) 110 } else { 111 log.Info("Mapped network port") 112 } 113 for { 114 select { 115 case _, ok := <-c: 116 if !ok { 117 return 118 } 119 case <-refresh.C: 120 log.Trace("Refreshing port mapping") 121 if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil { 122 log.Debug("Couldn't add port mapping", "err", err) 123 } 124 refresh.Reset(mapUpdateInterval) 125 } 126 } 127 } 128 129 // ExtIP assumes that the local machine is reachable on the given 130 // external IP address, and that any required ports were mapped manually. 131 // Mapping operations will not return an error but won't actually do anything. 132 func ExtIP(ip net.IP) Interface { 133 if ip == nil { 134 panic("IP must not be nil") 135 } 136 return extIP(ip) 137 } 138 139 type extIP net.IP 140 141 func (n extIP) ExternalIP() (net.IP, error) { return net.IP(n), nil } 142 func (n extIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) } 143 144 // These do nothing. 145 func (extIP) AddMapping(string, int, int, string, time.Duration) error { return nil } 146 func (extIP) DeleteMapping(string, int, int) error { return nil } 147 148 // Any returns a port mapper that tries to discover any supported 149 // mechanism on the local network. 150 func Any() Interface { 151 // TODO: attempt to discover whether the local machine has an 152 // Internet-class address. Return ExtIP in this case. 153 return startautodisc("UPnP or NAT-PMP", func() Interface { 154 found := make(chan Interface, 2) 155 go func() { found <- discoverUPnP() }() 156 go func() { found <- discoverPMP() }() 157 for i := 0; i < cap(found); i++ { 158 if c := <-found; c != nil { 159 return c 160 } 161 } 162 return nil 163 }) 164 } 165 166 // UPnP returns a port mapper that uses UPnP. It will attempt to 167 // discover the address of your router using UDP broadcasts. 168 func UPnP() Interface { 169 return startautodisc("UPnP", discoverUPnP) 170 } 171 172 // PMP returns a port mapper that uses NAT-PMP. The provided gateway 173 // address should be the IP of your router. If the given gateway 174 // address is nil, PMP will attempt to auto-discover the router. 175 func PMP(gateway net.IP) Interface { 176 if gateway != nil { 177 return &pmp{gw: gateway, c: natpmp.NewClient(gateway)} 178 } 179 return startautodisc("NAT-PMP", discoverPMP) 180 } 181 182 // autodisc represents a port mapping mechanism that is still being 183 // auto-discovered. Calls to the Interface methods on this type will 184 // wait until the discovery is done and then call the method on the 185 // discovered mechanism. 186 // 187 // This type is useful because discovery can take a while but we 188 // want return an Interface value from UPnP, PMP and Auto immediately. 189 type autodisc struct { 190 what string // type of interface being autodiscovered 191 once sync.Once 192 doit func() Interface 193 194 mu sync.Mutex 195 found Interface 196 } 197 198 func startautodisc(what string, doit func() Interface) Interface { 199 // TODO: monitor network configuration and rerun doit when it changes. 200 return &autodisc{what: what, doit: doit} 201 } 202 203 func (n *autodisc) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) error { 204 if err := n.wait(); err != nil { 205 return err 206 } 207 return n.found.AddMapping(protocol, extport, intport, name, lifetime) 208 } 209 210 func (n *autodisc) DeleteMapping(protocol string, extport, intport int) error { 211 if err := n.wait(); err != nil { 212 return err 213 } 214 return n.found.DeleteMapping(protocol, extport, intport) 215 } 216 217 func (n *autodisc) ExternalIP() (net.IP, error) { 218 if err := n.wait(); err != nil { 219 return nil, err 220 } 221 return n.found.ExternalIP() 222 } 223 224 func (n *autodisc) String() string { 225 n.mu.Lock() 226 defer n.mu.Unlock() 227 if n.found == nil { 228 return n.what 229 } else { 230 return n.found.String() 231 } 232 } 233 234 // wait blocks until auto-discovery has been performed. 235 func (n *autodisc) wait() error { 236 n.once.Do(func() { 237 n.mu.Lock() 238 n.found = n.doit() 239 n.mu.Unlock() 240 }) 241 if n.found == nil { 242 return fmt.Errorf("no %s router discovered", n.what) 243 } 244 return nil 245 }