// 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/ledgerwatch/log/v3" natpmp "github.com/jackpal/go-nat-pmp" ) // 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) error DeleteMapping(protocol string, extport, intport int) error SupportsMapping() bool // This method should return the external (Internet-facing) // address of the gateway device. ExternalIP() (net.IP, error) // 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 // "stun" uses STUN to detect an external IP using a default server // "stun:" uses STUN to detect an external IP using the given server (host:port) func Parse(spec string) (Interface, error) { var ( parts = strings.SplitN(spec, ":", 2) mech = strings.ToLower(parts[0]) ) switch mech { case "", "none", "off": return nil, nil case "any", "auto", "on": return Any(), nil case "extip", "ip": if len(parts) < 2 { return nil, errors.New("missing IP address") } ip := net.ParseIP(parts[1]) if ip == nil { return nil, errors.New("invalid IP address") } return ExtIP(ip), nil case "upnp": return UPnP(), nil case "pmp", "natpmp", "nat-pmp": var ip net.IP if len(parts) > 1 { ip = net.ParseIP(parts[1]) if ip == nil { return nil, errors.New("invalid IP address") } } return PMP(ip), nil case "stun": var addr string if len(parts) > 1 { addr = parts[1] } return NewSTUN(addr), nil default: return nil, fmt.Errorf("unknown mechanism %q", parts[0]) } } const ( mapTimeout = 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. func Map(m Interface, c <-chan struct{}, protocol string, extport, intport int, name string, logger log.Logger) { if !m.SupportsMapping() { panic("Port mapping is not supported") } logger1 := logger.New("proto", protocol, "extport", extport, "intport", intport, "interface", m) refresh := time.NewTimer(mapTimeout) defer func() { refresh.Stop() logger1.Trace("Deleting port mapping") m.DeleteMapping(protocol, extport, intport) }() if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil { logger1.Debug("Couldn't add port mapping", "err", err) } else { logger1.Info("Mapped network port") } for { select { case _, ok := <-c: if !ok { return } case <-refresh.C: logger1.Trace("Refreshing port mapping") if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); err != nil { logger1.Debug("Couldn't add port mapping", "err", err) } refresh.Reset(mapTimeout) } } } // 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) error { return nil } func (ExtIP) DeleteMapping(string, int, int) error { return nil } func (ExtIP) SupportsMapping() bool { return false } // 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) go func() { found <- discoverUPnP() }() go 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) error { if err := n.wait(); err != nil { return 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) SupportsMapping() bool { return true } 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 }