This repository has been archived by the owner on Feb 14, 2021. It is now read-only.
forked from NebulousLabs/go-upnp
-
Notifications
You must be signed in to change notification settings - Fork 1
/
upnp.go
157 lines (142 loc) · 5.18 KB
/
upnp.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
// package upnp provides a simple and opinionated interface to UPnP-enabled
// routers, allowing users to forward ports and discover their external IP
// address. Specific quirks:
//
// - When attempting to discover UPnP-enabled routers on the network, only the
// first such router is returned. If you have multiple routers, this may cause
// some trouble. But why would you do that?
//
// - Forwarded ports are always symmetric, e.g. the router's port 9980 will be
// mapped to the client's port 9980. This will be unacceptable for some
// purposes, but too bad. Symmetric mappings are the desired behavior 99% of
// the time, and they save a function argument.
//
// - TCP and UDP protocols are forwarded together.
//
// - Ports are forwarded permanently. Some other implementations lease a port
// mapping for a set duration, and then renew it periodically. This is nice,
// because it means mappings won't stick around after they've served their
// purpose. Unfortunately, some routers only support permanent mappings, so this
// package has chosen to support the lowest common denominator. To un-forward a
// port, you must use the Clear function (or do it manually).
//
// Once you've discovered your router, you can retrieve its address by calling
// its Location method. This address can be supplied to Load to connect to the
// router directly, which is much faster than calling Discover.
package upnp
import (
"errors"
"net"
"net/url"
"strings"
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway1"
)
// An IGD provides an interface to the most commonly used functions of an
// Internet Gateway Device: discovering the external IP, and forwarding ports.
type IGD interface {
ExternalIP() (string, error)
Forward(port uint16, description, proto string) error
Clear(port uint16, proto string) error
Location() string
}
// upnpDevice implements the IGD interface. It is essentially a bridge between
// IGD and the internetgateway1.WANIPConnection1 and
// internetgateway1.WANPPPConnection1 types.
type upnpDevice struct {
client interface {
GetExternalIPAddress() (string, error)
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
DeletePortMapping(string, uint16, string) error
GetServiceClient() *goupnp.ServiceClient
}
}
// ExternalIP returns the router's external IP.
func (u *upnpDevice) ExternalIP() (string, error) {
return u.client.GetExternalIPAddress()
}
// Forward forwards the specified port, and adds its description to the
// router's port mapping table.
//
// TODO: is desc necessary?
// TODO: take an int instead? More convenient.
// EDIT: now explicitly passing in protocol to open for
func (u *upnpDevice) Forward(port uint16, desc, proto string) error {
proto = strings.ToUpper(proto)
ip, err := u.getInternalIP()
if err != nil {
return err
}
return u.client.AddPortMapping("", port, proto, port, ip, true, desc, 0)
}
// Clear un-forwards a port, removing it from the router's port mapping table.
// EDIT: now passing in specific proto to clear
func (u *upnpDevice) Clear(port uint16, proto string) error {
proto = strings.ToUpper(proto)
return u.client.DeletePortMapping("", port, proto)
}
// Location returns the URL of the router, for future lookups (see Load).
func (u *upnpDevice) Location() string {
return u.client.GetServiceClient().Location.String()
}
// getInternalIP returns the user's local IP.
func (u *upnpDevice) getInternalIP() (string, error) {
host, _, _ := net.SplitHostPort(u.client.GetServiceClient().RootDevice.URLBase.Host)
devIP := net.ParseIP(host)
if devIP == nil {
return "", errors.New("could not determine router's internal IP")
}
ifaces, err := net.Interfaces()
if err != nil {
return "", err
}
for _, iface := range ifaces {
addrs, err := iface.Addrs()
if err != nil {
return "", err
}
for _, addr := range addrs {
switch x := addr.(type) {
case *net.IPNet:
if x.Contains(devIP) {
return x.IP.String(), nil
}
}
}
}
return "", errors.New("could not determine internal IP")
}
// Discover scans the local network for routers and returns the first
// UPnP-enabled router it encounters.
//
// TODO: if more than one client is found, only return those on the same
// subnet as the user?
func Discover() (IGD, error) {
pppclients, _, _ := internetgateway1.NewWANPPPConnection1Clients()
if len(pppclients) > 0 {
return &upnpDevice{pppclients[0]}, nil
}
ipclients, _, _ := internetgateway1.NewWANIPConnection1Clients()
if len(ipclients) > 0 {
return &upnpDevice{ipclients[0]}, nil
}
return nil, errors.New("no UPnP-enabled gateway found")
}
// Load connects to the router service specified by rawurl. This is much
// faster than Discover. Generally, Load should only be called with values
// returned by the IGD's Location method.
func Load(rawurl string) (IGD, error) {
loc, err := url.Parse(rawurl)
if err != nil {
return nil, err
}
pppclients, _ := internetgateway1.NewWANPPPConnection1ClientsByURL(loc)
if len(pppclients) > 0 {
return &upnpDevice{pppclients[0]}, nil
}
ipclients, _ := internetgateway1.NewWANIPConnection1ClientsByURL(loc)
if len(ipclients) > 0 {
return &upnpDevice{ipclients[0]}, nil
}
return nil, errors.New("no UPnP-enabled gateway found at URL " + rawurl)
}