// Copyright 2015 The go-ethereum Authors // This file is part of the go-ethereum library. // // The go-ethereum library is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // The go-ethereum library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . // Package nat provides access to common network port mapping protocols. package nat import ( "errors" "fmt" "net" "strings" "sync" "time" "github.com/ethereum/go-ethereum/common/gopool" "github.com/ethereum/go-ethereum/log" natpmp "github.com/jackpal/go-nat-pmp" ) // Interface An implementation of nat.Interface can map local ports to ports // accessible from the Internet. type Interface interface { // These methods manage a mapping between a port on the local // machine to a port that can be connected to from the internet. // // protocol is "UDP" or "TCP". Some implementations allow setting // a display name for the mapping. The mapping may be removed by // the gateway when its lifetime ends. AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error) DeleteMapping(protocol string, extport, intport int) error // ExternalIP should return the external (Internet-facing) // address of the gateway device. ExternalIP() (net.IP, error) // String should return name of the method. This is used for logging. String() string } // Parse parses a NAT interface description. // The following formats are currently accepted. // Note that mechanism names are not case-sensitive. // // "" or "none" return nil // "extip:77.12.33.4" will assume the local machine is reachable on the given IP // "any" uses the first auto-detected mechanism // "upnp" uses the Universal Plug and Play protocol // "pmp" uses NAT-PMP with an auto-detected gateway address // "pmp:192.168.0.1" uses NAT-PMP with the given gateway address func Parse(spec string) (Interface, error) { var ( before, after, found = strings.Cut(spec, ":") mech = strings.ToLower(before) ip net.IP ) if found { ip = net.ParseIP(after) if ip == nil { return nil, errors.New("invalid IP address") } } switch mech { case "", "none", "off": return nil, nil case "any", "auto", "on": return Any(), nil case "extip", "ip": if ip == nil { return nil, errors.New("missing IP address") } return ExtIP(ip), nil case "upnp": return UPnP(), nil case "pmp", "natpmp", "nat-pmp": return PMP(ip), nil default: return nil, fmt.Errorf("unknown mechanism %q", before) } } const ( DefaultMapTimeout = 10 * time.Minute ) // Map adds a port mapping on m and keeps it alive until c is closed. // This function is typically invoked in its own goroutine. // // Note that Map does not handle the situation where the NAT interface assigns a different // external port than the requested one. func Map(m Interface, c <-chan struct{}, protocol string, extport, intport int, name string) { log := log.New("proto", protocol, "extport", extport, "intport", intport, "interface", m) refresh := time.NewTimer(DefaultMapTimeout) defer func() { refresh.Stop() log.Debug("Deleting port mapping") m.DeleteMapping(protocol, extport, intport) }() if _, err := m.AddMapping(protocol, extport, intport, name, DefaultMapTimeout); err != nil { log.Debug("Couldn't add port mapping", "err", err) } else { log.Info("Mapped network port") } for { select { case _, ok := <-c: if !ok { return } case <-refresh.C: log.Trace("Refreshing port mapping") if _, err := m.AddMapping(protocol, extport, intport, name, DefaultMapTimeout); err != nil { log.Debug("Couldn't add port mapping", "err", err) } refresh.Reset(DefaultMapTimeout) } } } // ExtIP assumes that the local machine is reachable on the given // external IP address, and that any required ports were mapped manually. // Mapping operations will not return an error but won't actually do anything. type ExtIP net.IP func (n ExtIP) ExternalIP() (net.IP, error) { return net.IP(n), nil } func (n ExtIP) String() string { return fmt.Sprintf("ExtIP(%v)", net.IP(n)) } // These do nothing. func (ExtIP) AddMapping(string, int, int, string, time.Duration) (uint16, error) { return 0, nil } func (ExtIP) DeleteMapping(string, int, int) error { return nil } // Any returns a port mapper that tries to discover any supported // mechanism on the local network. func Any() Interface { // TODO: attempt to discover whether the local machine has an // Internet-class address. Return ExtIP in this case. return startautodisc("UPnP or NAT-PMP", func() Interface { found := make(chan Interface, 2) gopool.Submit(func() { found <- discoverUPnP() }) gopool.Submit(func() { found <- discoverPMP() }) for i := 0; i < cap(found); i++ { if c := <-found; c != nil { return c } } return nil }) } // UPnP returns a port mapper that uses UPnP. It will attempt to // discover the address of your router using UDP broadcasts. func UPnP() Interface { return startautodisc("UPnP", discoverUPnP) } // PMP returns a port mapper that uses NAT-PMP. The provided gateway // address should be the IP of your router. If the given gateway // address is nil, PMP will attempt to auto-discover the router. func PMP(gateway net.IP) Interface { if gateway != nil { return &pmp{gw: gateway, c: natpmp.NewClient(gateway)} } return startautodisc("NAT-PMP", discoverPMP) } // autodisc represents a port mapping mechanism that is still being // auto-discovered. Calls to the Interface methods on this type will // wait until the discovery is done and then call the method on the // discovered mechanism. // // This type is useful because discovery can take a while but we // want return an Interface value from UPnP, PMP and Auto immediately. type autodisc struct { what string // type of interface being autodiscovered once sync.Once doit func() Interface mu sync.Mutex found Interface } func startautodisc(what string, doit func() Interface) Interface { // TODO: monitor network configuration and rerun doit when it changes. return &autodisc{what: what, doit: doit} } func (n *autodisc) AddMapping(protocol string, extport, intport int, name string, lifetime time.Duration) (uint16, error) { if err := n.wait(); err != nil { return 0, err } return n.found.AddMapping(protocol, extport, intport, name, lifetime) } func (n *autodisc) DeleteMapping(protocol string, extport, intport int) error { if err := n.wait(); err != nil { return err } return n.found.DeleteMapping(protocol, extport, intport) } func (n *autodisc) ExternalIP() (net.IP, error) { if err := n.wait(); err != nil { return nil, err } return n.found.ExternalIP() } func (n *autodisc) String() string { n.mu.Lock() defer n.mu.Unlock() if n.found == nil { return n.what } return n.found.String() } // wait blocks until auto-discovery has been performed. func (n *autodisc) wait() error { n.once.Do(func() { n.mu.Lock() n.found = n.doit() n.mu.Unlock() }) if n.found == nil { return fmt.Errorf("no %s router discovered", n.what) } return nil }