下記のパッケージで機能が提供されています:
- https://github.com/huin/goupnp
- https://gitlab.com/NebulousLabs/go-upnp
- https://gitlab.com/NebulousLabs/fastrand
gitlab.com/NebulousLabs/go-upnp は、このままでは
現在の github.com/huin/goupnp とマッチしないので
ローカルの upnp/upnp.go に必要な箇所をコピーして修正して使用しました。
最低限のソースのみ
upnp/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 |
// 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 ( "context" "errors" "net/url" "time" "gitlab.com/NebulousLabs/fastrand" "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 struct { // This interface is satisfied by the internetgateway1.WANIPConnection1 // and internetgateway1.WANPPPConnection1 types. client interface { GetExternalIPAddress() (string, error) AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error GetSpecificPortMappingEntry(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 (d *IGD) ExternalIP() (string, error) { return d.client.GetExternalIPAddress() } // Forward forwards the specified protcol, port, and adds its description to the // router's port mapping table. func (d *IGD) Forward(routerPort uint16, protcol string, destIP string, destPort uint16, desc string) error { time.Sleep(time.Millisecond) return d.client.AddPortMapping("", routerPort, protcol, destPort, destIP, true, desc, 0) } // Clear un-forwards a port, removing it from the router's port mapping table. func (d *IGD) Clear(port uint16, protcol string) error { time.Sleep(time.Millisecond) return d.client.DeletePortMapping("", port, protcol) } // Discover is deprecated; use DiscoverCtx instead. func Discover() (*IGD, error) { return DiscoverCtx(context.Background()) } // DiscoverCtx scans the local network for routers and returns the first // UPnP-enabled router it encounters. It will try up to 3 times to find a // router, sleeping a random duration between each attempt. This is to // mitigate a race condition with many callers attempting to discover // simultaneously. func DiscoverCtx(ctx context.Context) (*IGD, error) { // TODO: if more than one client is found, only return those on the same // subnet as the user? maxTries := 3 sleepTime := time.Millisecond * time.Duration(fastrand.Intn(5000)) for try := 0; try < maxTries; try++ { pppclients, _, _ := internetgateway1.NewWANPPPConnection1Clients() if len(pppclients) > 0 { return &IGD{pppclients[0]}, nil } ipclients, _, _ := internetgateway1.NewWANIPConnection1Clients() if len(ipclients) > 0 { return &IGD{ipclients[0]}, nil } select { case <-ctx.Done(): return nil, context.Canceled case <-time.After(sleepTime): } sleepTime *= 2 } 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 &IGD{pppclients[0]}, nil } ipclients, _ := internetgateway1.NewWANIPConnection1ClientsByURL(loc) if len(ipclients) > 0 { return &IGD{ipclients[0]}, nil } return nil, errors.New("no UPnP-enabled gateway found at URL " + rawurl) } |
main.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 |
package main import ( "bufio" "fmt" "log" "os" "strings" "go-upnp-simple/upnp" ) func main() { localIPv4 := "192.168.1.14" var internalPort uint16 = 80 var externalPort uint16 = 8080 desc := "go upnp test" // UPnPデバイスの検索 igo, err := upnp.Discover() if err != nil { log.Println(err) return } // ルーターの外部IPアドレスを取得 externalIP, err := igo.ExternalIP() if err != nil { log.Println(err) return } // ポートマッピングを追加 TCP err = igo.Forward(externalPort, "TCP", localIPv4, internalPort, desc) if err != nil { log.Println("error forwarding port", err) return } // ポートマッピングを追加 UDP err = igo.Forward(externalPort, "UDP", localIPv4, internalPort, desc) if err != nil { log.Println("error forwarding port", err) return } log.Printf("From external %s:%d forwarded to local %s:%d\n", externalIP, externalPort, localIPv4, internalPort) stringPrompt("Press enter key to exit...") // ポートマッピングを削除 TCP err = igo.Clear(externalPort, "TCP") if err != nil { log.Println(err) } // ポートマッピングを削除 UDP err = igo.Clear(externalPort, "UDP") if err != nil { log.Println(err) } } // stringPrompt asks for a string value using the label // https://dev.to/tidalcloud/interactive-cli-prompts-in-go-3bj9 func stringPrompt(label string) string { var s string r := bufio.NewReader(os.Stdin) for { fmt.Fprint(os.Stderr, label+" ") s, _ = r.ReadString('\n') if s != "" { break } } return strings.TrimSpace(s) } |