This repository has been archived by the owner on Nov 2, 2018. It is now read-only.
/
upnp.go
219 lines (199 loc) · 5.66 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package host
import (
"context"
"errors"
"io"
"io/ioutil"
"net"
"net/http"
"strconv"
"strings"
"time"
"github.com/NebulousLabs/Sia/build"
"github.com/NebulousLabs/Sia/modules"
"github.com/NebulousLabs/go-upnp"
)
// managedLearnHostname discovers the external IP of the Host. If the host's
// net address is blank and the host's auto address appears to have changed,
// the host will make an announcement on the blockchain.
func (h *Host) managedLearnHostname() {
if build.Release == "testing" {
return
}
// Fetch a group of host vars that will be used to dictate the logic of the
// function.
h.mu.RLock()
netAddr := h.settings.NetAddress
hostPort := h.port
hostAutoAddress := h.autoAddress
hostAnnounced := h.announced
hostAcceptingContracts := h.settings.AcceptingContracts
hostContractCount := h.financialMetrics.ContractCount
h.mu.RUnlock()
// If the settings indicate that an address has been manually set, there is
// no reason to learn the hostname.
if netAddr != "" {
return
}
h.log.Println("No manually set net address. Scanning to automatically determine address.")
// try UPnP first, then fallback to myexternalip.com
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case <-h.tg.StopChan():
cancel()
case <-ctx.Done():
}
}()
var hostname string
d, err := upnp.DiscoverCtx(ctx)
if err == nil {
hostname, err = d.ExternalIP()
}
if err != nil {
hostname, err = myExternalIP()
}
if err != nil {
h.log.Println("WARN: failed to discover external IP")
return
}
autoAddress := modules.NetAddress(net.JoinHostPort(hostname, hostPort))
if err := autoAddress.IsValid(); err != nil {
h.log.Printf("WARN: discovered hostname %q is invalid: %v", autoAddress, err)
return
}
if autoAddress == hostAutoAddress && hostAnnounced {
// Nothing to do - the auto address has not changed and the previous
// annoucement was successful.
return
}
h.mu.Lock()
h.autoAddress = autoAddress
err = h.saveSync()
h.mu.Unlock()
if err != nil {
h.log.Println(err)
}
// Announce the host, but only if the host is either accepting contracts or
// has a storage obligation. If the host is not accepting contracts and has
// no open contracts, there is no reason to notify anyone that the host's
// address has changed.
if hostAcceptingContracts || hostContractCount > 0 {
h.log.Println("Host external IP address changed from", hostAutoAddress, "to", autoAddress, "- performing host announcement.")
err = h.managedAnnounce(autoAddress)
if err != nil {
// Set h.announced to false, as the address has changed yet the
// renewed annoucement has failed.
h.mu.Lock()
h.announced = false
h.mu.Unlock()
h.log.Println("unable to announce address after upnp-detected address change:", err)
}
}
}
// managedForwardPort adds a port mapping to the router.
func (h *Host) managedForwardPort(port string) error {
if build.Release == "testing" {
// Add a blocking placeholder where testing is able to mock behaviors
// such as a port forward action that blocks for 10 seconds before
// completing.
if h.dependencies.Disrupt("managedForwardPort") {
return nil
}
// Port forwarding functions are frequently unavailable during testing,
// and the long blocking can be highly disruptive. Under normal
// scenarios, return without complaint, and without running the
// port-forward logic.
return nil
}
// If the port is invalid, there is no need to perform any of the other
// tasks.
portInt, err := strconv.Atoi(port)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case <-h.tg.StopChan():
cancel()
case <-ctx.Done():
}
}()
d, err := upnp.DiscoverCtx(ctx)
if err != nil {
h.log.Printf("WARN: could not automatically forward port %s: %v", port, err)
return err
}
err = d.Forward(uint16(portInt), "Sia Host")
if err != nil {
h.log.Printf("WARN: could not automatically forward port %s: %v", port, err)
return err
}
h.log.Println("INFO: successfully forwarded port", port)
return nil
}
// managedClearPort removes a port mapping from the router.
func (h *Host) managedClearPort() error {
if build.Release == "testing" {
// Allow testing to force an error to be returned here.
if h.dependencies.Disrupt("managedClearPort return error") {
return errors.New("Mocked managedClearPortErr")
}
return nil
}
// If the port is invalid, there is no need to perform any of the other
// tasks.
h.mu.RLock()
port := h.port
h.mu.RUnlock()
portInt, err := strconv.Atoi(port)
if err != nil {
return err
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
select {
case <-h.tg.StopChan():
cancel()
case <-ctx.Done():
}
}()
d, err := upnp.DiscoverCtx(ctx)
if err != nil {
return err
}
err = d.Clear(uint16(portInt))
if err != nil {
return err
}
h.log.Println("INFO: successfully unforwarded port", port)
return nil
}
// myExternalIP discovers the host's external IP by querying a centralized
// service, http://myexternalip.com.
func myExternalIP() (string, error) {
// timeout after 10 seconds
client := http.Client{Timeout: time.Duration(10 * time.Second)}
resp, err := client.Get("http://myexternalip.com/raw")
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
errResp, _ := ioutil.ReadAll(resp.Body)
return "", errors.New(string(errResp))
}
buf, err := ioutil.ReadAll(io.LimitReader(resp.Body, 64))
if err != nil {
return "", err
}
if len(buf) == 0 {
return "", errors.New("myexternalip.com returned a 0 length IP address")
}
// trim newline
return strings.TrimSpace(string(buf)), nil
}