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())
+}