diff --git a/Network Detection.go b/Network Detection.go index 66bc6ff..7999126 100644 --- a/Network Detection.go +++ b/Network Detection.go @@ -171,7 +171,11 @@ func networkChangeMonitor() { func networkChangeInterfaceNew(iface net.Interface, addresses []net.Addr) { log.Printf("networkChangeInterfaceNew new interface '%s' (%d IPs)\n", iface.Name, len(addresses)) - networkStart(iface, addresses) + networks := networkStart(iface, addresses) + + for _, network := range networks { + go network.upnpAuto() + } go nodesDHT.RefreshBuckets(0) } @@ -214,7 +218,11 @@ func networkChangeInterfaceRemove(iface string, addresses []net.Addr) { func networkChangeIPNew(iface net.Interface, address net.Addr) { log.Printf("networkChangeIPNew new interface '%s' IP %s\n", iface.Name, address.String()) - networkStart(iface, []net.Addr{address}) + networks := networkStart(iface, []net.Addr{address}) + + for _, network := range networks { + go network.upnpAuto() + } go nodesDHT.RefreshBuckets(0) } diff --git a/Network Init.go b/Network Init.go index 1025615..1994611 100644 --- a/Network Init.go +++ b/Network Init.go @@ -107,7 +107,7 @@ func initNetwork() { } // networkStart will start the listeners on all the IP addresses for the network -func networkStart(iface net.Interface, addresses []net.Addr) { +func networkStart(iface net.Interface, addresses []net.Addr) (networks []*Network) { for _, address := range addresses { net1 := address.(*net.IPNet) @@ -133,7 +133,11 @@ func networkStart(iface net.Interface, addresses []net.Addr) { addListenAddress(netw.address) log.Printf("Listen on network '%s' UDP %s\n", iface.Name, netw.address.String()) + + networks = append(networks, netw) } + + return } // networkPrepareListen prepares to listen on the given IP address. If port is 0, one is chosen automatically. @@ -147,7 +151,7 @@ func networkPrepareListen(ipA string, port int) (network *Network, err error) { network.terminateSignal = make(chan interface{}) // get the network interface that belongs to the IP - if ipA != "0.0.0.0" && ipA != "::" { + if !ip.IsUnspecified() { // checks for IPv4 "0.0.0.0" and IPv6 "::" network.iface, network.ipnet = FindInterfaceByIP(ip) if network.iface == nil { return nil, errors.New("error finding the network interface belonging to IP") diff --git a/Network UPnP.go b/Network UPnP.go new file mode 100644 index 0000000..7938b44 --- /dev/null +++ b/Network UPnP.go @@ -0,0 +1,165 @@ +/* +File Name: Network UPnP.go +Copyright: 2021 Peernet s.r.o. +Author: Peter Kleissner + +Currently only supports IPv4 networks. +TODO: Limit mapping to X hours. Auto remap upon expiration. +*/ + +package core + +import ( + "log" + "math/rand" + "net" + "sync" + "time" + + "github.com/PeernetOfficial/core/upnp" +) + +var upnpListInterfaces map[string]struct{} +var upnpMutex sync.RWMutex + +func startUPnP() { + upnpListInterfaces = make(map[string]struct{}) + + if config.PortForward > 0 { + config.EnableUPnP = false + } + if !config.EnableUPnP { + return + } + + for _, network := range networks4 { + go network.upnpAuto() + } +} + +// upnpIsEligible checks if the network is eligible for UPnP +func (network *Network) upnpIsEligible() bool { + // No link-local addresses. 169.254.*.* + // No loopback. + if network.address.IP.IsLinkLocalUnicast() || network.address.IP.IsLoopback() { + return false + } + + // The network interface must be known which indicates that the IP address is NOT a wildcard. + // Port forwarding requires to specify a local IP. In case of listening on a wildcard that would not be known and guessing (looking at you btcd) is not a solution. + if network.iface == nil { + return false + } + + return true +} + +// upnpAuto runs a UPnP daemon to forward the port, refresh the forwarding and continuously monitor if the forwarding remains valid. +func (network *Network) upnpAuto() { + if !config.EnableUPnP || !network.upnpIsEligible() { + return + } + + // Only allow 1 UPnP worker at a time. + upnpMutex.Lock() + defer upnpMutex.Unlock() + + // If there is already a running UPnP on the adapter, skip. + if _, ok := upnpListInterfaces[network.GetAdapterName()]; ok { + return + } + + nat, err := upnp.Discover(network.address.IP) + if err != nil { + return + } + + network.nat = nat + + externalIP, err := nat.GetExternalAddress() + if err != nil { + return + } + + network.ipExternal = externalIP + + if err := network.upnpTryPortForward(); err != nil { + return + } + + upnpListInterfaces[network.GetAdapterName()] = struct{}{} + + go network.upnpMonitorPortForward() +} + +// upnpMonitorPortForward monitors the port forwarding status +func (network *Network) upnpMonitorPortForward() { + ticker := time.NewTicker(time.Second * 10) + +monitorLoop: + for { + select { + case <-ticker.C: + case <-network.terminateSignal: + // Remove port mapping. Note that in case the network is unavailable this is likely to fail. + network.nat.DeletePortMapping("UDP", network.portExternal) + + network.portExternal = 0 + network.ipExternal = net.IP{} + + break monitorLoop + } + + // 3 tries + var err error + for n := 0; n < 3; n++ { + if err = network.upnpValidate(); err == nil { + continue monitorLoop + } + } + + // invalid :( + log.Printf("upnpAuto port forwarding invalidated for local IP %s (adapter %s) external IP %s port %d", network.address.String(), network.iface.Name, network.ipExternal.String(), network.portExternal) + + network.portExternal = 0 + network.ipExternal = net.IP{} + + break + } + + ticker.Stop() + + upnpMutex.Lock() + delete(upnpListInterfaces, network.GetAdapterName()) + upnpMutex.Unlock() +} + +func (network *Network) upnpTryPortForward() (err error) { + // Try forwarding the port. First to the same one listening, otherwise random. + mappedExternalPort, err := network.nat.AddPortMapping("UDP", network.address.IP, uint16(network.address.Port), uint16(network.address.Port), "Peernet", 0) + if err != nil { + mappedExternalPort, err = network.nat.AddPortMapping("UDP", network.address.IP, uint16(network.address.Port), uint16(randInt(1024, 65535)), "Peernet", 0) + } + if err != nil { + return err + } + + // validate + if err := network.upnpValidate(); err != nil { + return err + } + + // valid! + network.portExternal = mappedExternalPort + + return nil +} + +func randInt(min int, max int) int { + return min + rand.Intn(max-min) +} + +func (network *Network) upnpValidate() (err error) { + // TODO: Send special message which validates the UPnP mapping + return nil +} diff --git a/Network.go b/Network.go index d311710..4ca99ea 100644 --- a/Network.go +++ b/Network.go @@ -13,6 +13,8 @@ import ( "sync" "sync/atomic" "time" + + "github.com/PeernetOfficial/core/upnp" ) // Network is a connection adapter through one network interface (adapter). @@ -27,6 +29,8 @@ type Network struct { broadcastSocket net.PacketConn // Broadcast socket, IPv4 only. broadcastIPv4 []net.IP // Broadcast IPs, IPv4 only. portExternal uint16 // External port. 0 if not known. + ipExternal net.IP // External IP of the network. Usually not known. + nat upnp.NAT // UPnP: NAT information isTerminated bool // If true, the network was signaled for termination terminateSignal chan interface{} // gets closed on termination signal, can be used in select via "case _ = <- network.terminateSignal:" sync.RWMutex // for sychronized closing diff --git a/Peernet.go b/Peernet.go index 86be704..aef0a2f 100644 --- a/Peernet.go +++ b/Peernet.go @@ -26,4 +26,5 @@ func Connect() { go autoPingAll() go networkChangeMonitor() go autoBucketRefresh() + go startUPnP() } diff --git a/go.mod b/go.mod index bd2c872..7b6cc53 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/PeernetOfficial/core go 1.16 require ( - github.com/PeernetOfficial/core/dht v0.0.0-20210425014452-611dfa3b1ce9 + github.com/PeernetOfficial/core/dht v0.0.0-20210506124834-e929d6ba2f22 github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025 github.com/pkg/errors v0.9.1 golang.org/x/crypto v0.0.0-20210415154028-4f45737414dc diff --git a/go.sum b/go.sum index 8c33674..48207c5 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/PeernetOfficial/core/dht v0.0.0-20210425014452-611dfa3b1ce9 h1:kcjoWZl9GZ9uSajS6t0tiKDpaatUQ64tl3OqXPc+YsY= -github.com/PeernetOfficial/core/dht v0.0.0-20210425014452-611dfa3b1ce9/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw= +github.com/PeernetOfficial/core/dht v0.0.0-20210506124834-e929d6ba2f22 h1:FHBHtQR3ShxuxkG8tgE4ER86UmbNdXam/I1irdiusqg= +github.com/PeernetOfficial/core/dht v0.0.0-20210506124834-e929d6ba2f22/go.mod h1:sb2H21VIVTohCXVrDFo/Skl2tN8Yzba+MXSS6NgCYXw= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta.0.20210401013323-36a96f6a0025 h1:aoVqvZk4mLyF3WZbqEVPq+vXnwL2wekZg4P4mjYJNLs= diff --git a/upnp/README.md b/upnp/README.md index fe5be4e..f0e063a 100644 --- a/upnp/README.md +++ b/upnp/README.md @@ -3,3 +3,18 @@ There are 2 reference implementations which both are based on 'Taipei Torrent'. This UPnP code is a fork mostly from the btcd one with some changes forked from tendermint. * https://github.com/btcsuite/btcd/blob/master/upnp.go * https://github.com/tendermint/tendermint/tree/master/p2p/upnp + +This library supports only IPv4 UPnP currently. The IPv6 UPnP protocol is specified here: http://upnp.org/specs/arch/UPnP-arch-AnnexAIPv6-v1.pdf + +## Special Cases + +FritzBox: +* Users must manually enable port sharing for the host. Otherwise the router returns XML error code 606. +* If the internal port is already forwarded under a different external port, error code 718 is returned. +* If the internal port is already forwarded under the same external port, it does not return an error. + +## Troubleshooting + +Using Wireshark to intercept the UPnP request and response can help. + +For Windows there is a UPnP tool here http://miniupnp.free.fr/files/. The tool supports listing existing mappings which is an interesting functionality. It would make sense to implement the list function to delete or reuse any existing mappings. diff --git a/upnp/UPnP.go b/upnp/UPnP.go index 32238bb..f1bfd67 100644 --- a/upnp/UPnP.go +++ b/upnp/UPnP.go @@ -17,31 +17,31 @@ import ( "time" ) -// NAT is an interface representing a NAT traversal options for example UPNP or -// NAT-PMP. It provides methods to query and manipulate this traversal to allow -// access to services. +// NAT is an interface representing a NAT traversal options for example UPNP or NAT-PMP. +// It provides methods to query and manipulate this traversal to allow access to services. type NAT interface { // Get the external address from outside the NAT. GetExternalAddress() (addr net.IP, err error) // Add a port mapping for protocol ("udp" or "tcp") from external port to internal port with description lasting for timeout. - AddPortMapping(protocol string, internalIP net.IP, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) + AddPortMapping(protocol string, internalIP net.IP, internalPort, externalPort uint16, description string, timeout int) (mappedExternalPort uint16, err error) // Remove a previously added port mapping from external port to internal port. - DeletePortMapping(protocol string, externalPort, internalPort int) (err error) + DeletePortMapping(protocol string, externalPort uint16) (err error) } type upnpNAT struct { serviceURL string urnDomain string + localIP net.IP } -// Discover searches the local network for a UPnP router returning a NAT -// for the network if so, nil if not. -func Discover() (nat NAT, err error) { +// Discover searches the local network for a UPnP router returning a NAT for the network if so, nil if not. +// Socket must be an active local socket. +func Discover(localIP net.IP) (nat NAT, err error) { ssdp, err := net.ResolveUDPAddr("udp4", "239.255.255.250:1900") if err != nil { return } - conn, err := net.ListenPacket("udp4", ":0") + conn, err := net.ListenPacket("udp4", net.JoinHostPort(localIP.String(), "0")) // use a random port if err != nil { return } @@ -97,11 +97,11 @@ func Discover() (nat NAT, err error) { } locURL := strings.TrimSpace(loc[0:endIndex]) var serviceURL, urnDomain string - serviceURL, urnDomain, err = getServiceURL(locURL) + serviceURL, urnDomain, err = getServiceURL(localIP, locURL) if err != nil { return } - nat = &upnpNAT{serviceURL: serviceURL, urnDomain: urnDomain} + nat = &upnpNAT{serviceURL: serviceURL, urnDomain: urnDomain, localIP: localIP} return } } @@ -185,8 +185,25 @@ func getChildService(d *device, serviceType string) *service { // getServiceURL parses the xml description at the given root url to find the // url for the WANIPConnection service to be used for port forwarding. -func getServiceURL(rootURL string) (url, urnDomain string, err error) { - r, err := http.Get(rootURL) +func getServiceURL(localIP net.IP, rootURL string) (url, urnDomain string, err error) { + + webclient := &http.Client{ + Transport: &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{ + LocalAddr: &net.TCPAddr{ + IP: localIP, + }, + Timeout: 3 * time.Second, + DualStack: true, + }).DialContext, + TLSHandshakeTimeout: 3 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + Timeout: 3 * time.Second, + } + + r, err := webclient.Get(rootURL) if err != nil { return } @@ -259,7 +276,7 @@ type soapEnvelope struct { // soapRequests performs a soap request with the given parameters and returns // the xml replied stripped of the soap headers. in the case that the request is // unsuccessful the an error is returned. -func soapRequest(url, function, message, domain string) (replyXML []byte, err error) { +func (n *upnpNAT) soapRequest(url, function, message, domain string) (replyXML []byte, err error) { fullMessage := "" + "\r\n" + "" + message + "" @@ -275,7 +292,23 @@ func soapRequest(url, function, message, domain string) (replyXML []byte, err er req.Header.Set("Cache-Control", "no-cache") req.Header.Set("Pragma", "no-cache") - r, err := http.DefaultClient.Do(req) + webclient := &http.Client{ + Transport: &http.Transport{ + Proxy: nil, + DialContext: (&net.Dialer{ + LocalAddr: &net.TCPAddr{ + IP: n.localIP, + }, + Timeout: 3 * time.Second, + DualStack: true, + }).DialContext, + TLSHandshakeTimeout: 3 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + }, + Timeout: 3 * time.Second, + } + + r, err := webclient.Do(req) if err != nil { return nil, err } @@ -307,7 +340,7 @@ type getExternalIPAddressResponse struct { // from the UPnP router. func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { message := "\r\n" - response, err := soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) + response, err := n.soapRequest(n.serviceURL, "GetExternalIPAddress", message, n.urnDomain) if err != nil { return nil, err } @@ -325,22 +358,25 @@ func (n *upnpNAT) GetExternalAddress() (addr net.IP, err error) { return addr, nil } -// AddPortMapping implements the NAT interface by setting up a port forwarding -// from the UPnP router to the local machine with the given ports and protocol. -func (n *upnpNAT) AddPortMapping(protocol string, internalIP net.IP, externalPort, internalPort int, description string, timeout int) (mappedExternalPort int, err error) { +// AddPortMapping forwards a port at the UPnP router to the specified IP address and port. Lease duration is in seconds. +// FritzBox routers: Forwarding an already forwarded port results in no error. If the internal port is already forwarded under a different external port, error code 718 is returned in XML. +func (n *upnpNAT) AddPortMapping(protocol string, internalIP net.IP, internalPort, externalPort uint16, description string, leaseDuration int) (mappedExternalPort uint16, err error) { // A single concatenation would break ARM compilation. message := "\r\n" + - "" + strconv.Itoa(externalPort) + "" + strconv.Itoa(int(externalPort)) message += "" + strings.ToUpper(protocol) + "" - message += "" + strconv.Itoa(internalPort) + "" + + message += "" + strconv.Itoa(int(internalPort)) + "" + "" + internalIP.String() + "" + "1" message += description + - "" + strconv.Itoa(timeout) + + "" + strconv.Itoa(leaseDuration) + "" - response, err := soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) + response, err := n.soapRequest(n.serviceURL, "AddPortMapping", message, n.urnDomain) if err != nil { + // If UPnP is not allowed for the host (with FritzBox routers the user must manually enable it), the router returns "606" + // in the XML response with HTTP code 500 Internal Server Error. + // If the internal port is already forwarded under a different external port, error code 718 is returned in XML. return } @@ -352,16 +388,15 @@ func (n *upnpNAT) AddPortMapping(protocol string, internalIP net.IP, externalPor return mappedExternalPort, err } -// DeletePortMapping implements the NAT interface by removing up a port forwarding -// from the UPnP router to the local machine with the given ports and. -func (n *upnpNAT) DeletePortMapping(protocol string, externalPort, internalPort int) (err error) { +// DeletePortMapping deletes a port mapping. +func (n *upnpNAT) DeletePortMapping(protocol string, externalPort uint16) (err error) { message := "\r\n" + - "" + strconv.Itoa(externalPort) + + "" + strconv.Itoa(int(externalPort)) + "" + strings.ToUpper(protocol) + "" + "" - response, err := soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) + response, err := n.soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain) if err != nil { return } diff --git a/upnp/UPnP_test.go b/upnp/UPnP_test.go new file mode 100644 index 0000000..60f5d05 --- /dev/null +++ b/upnp/UPnP_test.go @@ -0,0 +1,27 @@ +// Small code for manual tests/development. + +package upnp + +import ( + "fmt" + "net" + "testing" +) + +func TestUPnP(t *testing.T) { + localIP := net.ParseIP("0.0.0.0") + + nat, err := Discover(localIP) + if err != nil { + fmt.Printf("%v\n", err) + return + } + + addr, err := nat.GetExternalAddress() + if err != nil { + fmt.Printf("%v\n", err) + return + } + + fmt.Printf("%s\n", addr.String()) +}