Working UPnP code. Close #16

This commit is contained in:
Kleissner
2021-05-06 14:57:40 +02:00
parent eb0afce3f9
commit 5f8f299799
10 changed files with 294 additions and 35 deletions

View File

@@ -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)
}

View File

@@ -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")

165
Network UPnP.go Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -26,4 +26,5 @@ func Connect() {
go autoPingAll()
go networkChangeMonitor()
go autoBucketRefresh()
go startUPnP()
}

2
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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.

View File

@@ -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 := "<?xml version=\"1.0\" ?>" +
"<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n" +
"<s:Body>" + message + "</s:Body></s:Envelope>"
@@ -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 := "<u:GetExternalIPAddress xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n</u:GetExternalIPAddress>"
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 := "<u:AddPortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort)
"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(int(externalPort))
message += "</NewExternalPort><NewProtocol>" + strings.ToUpper(protocol) + "</NewProtocol>"
message += "<NewInternalPort>" + strconv.Itoa(internalPort) + "</NewInternalPort>" +
message += "<NewInternalPort>" + strconv.Itoa(int(internalPort)) + "</NewInternalPort>" +
"<NewInternalClient>" + internalIP.String() + "</NewInternalClient>" +
"<NewEnabled>1</NewEnabled><NewPortMappingDescription>"
message += description +
"</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(timeout) +
"</NewPortMappingDescription><NewLeaseDuration>" + strconv.Itoa(leaseDuration) +
"</NewLeaseDuration></u:AddPortMapping>"
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 "<errorCode>606</errorCode>"
// 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 := "<u:DeletePortMapping xmlns:u=\"urn:" + n.urnDomain + ":service:WANIPConnection:1\">\r\n" +
"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(externalPort) +
"<NewRemoteHost></NewRemoteHost><NewExternalPort>" + strconv.Itoa(int(externalPort)) +
"</NewExternalPort><NewProtocol>" + strings.ToUpper(protocol) + "</NewProtocol>" +
"</u:DeletePortMapping>"
response, err := soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain)
response, err := n.soapRequest(n.serviceURL, "DeletePortMapping", message, n.urnDomain)
if err != nil {
return
}

27
upnp/UPnP_test.go Normal file
View File

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