관리 메뉴

HAMA 블로그

[이더리움에서 배우는 Go언어] nat 옵션 이야기 - (2) 본문

Go

[이더리움에서 배우는 Go언어] nat 옵션 이야기 - (2)

[하마] 이승현 (wowlsh93@gmail.com) 2018. 12. 14. 16:40


        NAT-PNP 와 UPNP 를 이용한 홀펀칭 - (2) 


이번글에서는 이더리움에서 extip와 UPNP를 어떻게 사용하는지 배워보도록 하자. 

외부에서 192.168.10.11:80 서버와 통신하려면  그 사설 IP를 입력해바짜 아무 의미가 없다. 따라서 그 서버와 연결시켜주는 라우터를 통하게 되는데, 라우터의 공인IP인 193.24.171.247 로 보내되, 포트를 8028로 한다면 그것을 라우터는 192.168.10.11:80으로 포트포워딩해주는 것이다. 

즉 8028 포트는 내부의 192.168.10.12:80 IP/PORT와 매핑되는데 이것을 수동으로 미리 해 놓는다면 외부에서 접속하는데 아무 문제가 없을 것이다. 이렇게 수동으로 미리 매핑되어 있으면 extip를 그냥 사용하면 되고, 미리 정해져 있지 않아서 이더리움 프로그램에서 직접 매핑 설정을 해주려면 UPNP가 필요한 것이다. 

따라서 앞으로 살펴볼 코드를 예상해 볼 수 있는 것이, extip 는 특별히 하는것이 없을 것이며, 그냥 정해져있는 외부 IP를 사용만 할 것이며, (따라서 매핑 코드가 필요 없다) UPNP 는 위의 그림에서 라우터의 공인IP를 찾아서 외부IP로 설정하고, 특정 포트를 자신과 연결해주는 작업을 하는 코드가 있을 것이다. 

extip


// 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.
func ExtIP(ip net.IP) Interface {
if ip == nil {
panic("IP must not be nil")
}
return extIP(ip)
}

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 }

역시나 AddMapping / DeleteMapping처럼 매핑관련 메소드는 아무것도 안하며, 공인(외부)IP를 리턴해주는 ExternalIP 메소드는 미리 라우터에서 설정을 하여 nat 옵션으로 넣어준 ip 값을 그대로 사용한다. 

다음으로는 어디에서 AddMapping와 ExternalIP가 사용하는지 살펴보자.

if srv.NAT != nil {
if !realaddr.IP.IsLoopback() {
go nat.Map(srv.NAT, srv.quit, "udp", realaddr.Port, realaddr.Port, "ethereum discovery")
}
if ext, err := srv.NAT.ExternalIP(); err == nil {
realaddr = &net.UDPAddr{IP: ext, Port: realaddr.Port}
}
}

위 코드는 Server.Start() 메소드 안에 있는데 NAT옵션이 있는 경우 

1. nat.Map 메소드를 이용해서 실제port (이더리움 기본 포트는 30303) 와 NAT 서비스의 port 를 동일한 숫자로 매핑시켜준다. 


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(mapUpdateInterval)
defer func() {
refresh.Stop()
log.Debug("Deleting port mapping")
m.DeleteMapping(protocol, extport, intport)
}()
if err := m.AddMapping(protocol, extport, intport, name, mapTimeout); 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, mapTimeout); err != nil {
log.Debug("Couldn't add port mapping", "err", err)
}
refresh.Reset(mapUpdateInterval)
}
}
}

UPNP나 NAT-PMP의 AddMapping 메소드를 호출하여 실제 nat 서버와 통신하여 설정해준다.extip는 위에서 언급한 것 처럼 아무것도 안하며 UPNP에서 어떻게 작동하는지에 대한 코드는 UPNP목차에서 살펴본다.

2. 외부ip를 가져와서 net.UDPAddr 를 만들어 줘서 realaddr 로 사용한다. 


// node table
if !srv.NoDiscovery {
cfg := discover.Config{
PrivateKey: srv.PrivateKey,
AnnounceAddr: realaddr,
NodeDBPath: srv.NodeDatabase,
NetRestrict: srv.NetRestrict,
Bootnodes: srv.BootstrapNodes,
Unhandled: unhandled,
}
ntab, err := discover.ListenUDP(conn, cfg)
if err != nil {
return err
}
srv.ntab = ntab
}

해당 addr 는 노드디스커버리를 위해 AnnounceAddr로 설정되는데 이것은 다른 이더리움 사용자 노드들이 자신의 노드디스커버리테이블에 넣을 주변노드정보에 사용 될 것이다. 


// ListenUDP returns a new table that listens for UDP packets on laddr.
func ListenUDP(c conn, cfg Config) (*Table, error) {
tab, _, err := newUDP(c, cfg)
if err != nil {
return nil, err
}
log.Info("UDP listener up", "self", tab.self)
return tab, nil
}

func newUDP(c conn, cfg Config) (*Table, *udp, error) {
udp := &udp{
conn: c,
priv: cfg.PrivateKey,
netrestrict: cfg.NetRestrict,
closing: make(chan struct{}),
gotreply: make(chan reply),
addpending: make(chan *pending),
}
realaddr := c.LocalAddr().(*net.UDPAddr)
if cfg.AnnounceAddr != nil {
realaddr = cfg.AnnounceAddr
}
// TODO: separate TCP port
udp.ourEndpoint = makeEndpoint(realaddr, uint16(realaddr.Port))
tab, err := newTable(udp, PubkeyID(&cfg.PrivateKey.PublicKey), realaddr, cfg.NodeDBPath, cfg.Bootnodes)
if err != nil {
return nil, nil, err
}
udp.Table = tab

go udp.loop()
go udp.readLoop(cfg.Unhandled)
return udp.Table, udp, nil
}

AnnounceAddr는 realaddr로 대입되며, Endpoing로 세팅되고, 


func newTable(t transport, ourID NodeID, ourAddr *net.UDPAddr, nodeDBPath string, bootnodes []*Node) (*Table, error) {
// If no node database was given, use an in-memory one
db, err := newNodeDB(nodeDBPath, Version, ourID)
if err != nil {
return nil, err
}
tab := &Table{
net: t,
db: db,
self: NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)),
bonding: make(map[NodeID]*bondproc),
bondslots: make(chan struct{}, maxBondingPingPongs),
refreshReq: make(chan chan struct{}),
initDone: make(chan struct{}),
closeReq: make(chan struct{}),
closed: make(chan struct{}),
rand: mrand.New(mrand.NewSource(0)),
ips: netutil.DistinctNetSet{Subnet: tableSubnet, Limit: tableIPLimit},
}
if err := tab.setFallbackNodes(bootnodes); err != nil {
return nil, err
}
for i := 0; i < cap(tab.bondslots); i++ {
tab.bondslots <- struct{}{}
}
for i := range tab.buckets {
tab.buckets[i] = &bucket{
ips: netutil.DistinctNetSet{Subnet: bucketSubnet, Limit: bucketIPLimit},
}
}
tab.seedRand()
tab.loadSeedNodes(false)
// Start the background expiration goroutine after loading seeds so that the search for
// seed nodes also considers older nodes that would otherwise be removed by the
// expiration.
tab.db.ensureExpirer()
go tab.loop()
return tab, nil
}

  self:  NewNode(ourID, ourAddr.IP, uint16(ourAddr.Port), uint16(ourAddr.Port)) 로 노드디스커버리테이블에 세팅된다. Kademlia DHT알고리즘에 따라 해당 테이블에 대한 정보는 전세계에 얽혀있는 이더리움 네트워크간에 공유된다. 

UPNP

다시 언급하지만 UPNP의 핵심은 NAT서비스의 외부IP를 가져오고,포트를 로컬IP/PORT로 매핑하는 것이다.

먼저 nat 공통 인터페이스를 구현한 UPnP객체를 만들어 보자. 


// 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)
}

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}
}

UPnP 생성 함수를 호출하면 내부적으로 autodisc 객체가 만들어 지는데  UPnP라는 문자열 이름과, 진짜 생성 함수가 매개변수로 들어가서 doit 변수에 대입된다. 이 doit (discoverUPnP) 을 통해 nat 인터페이스 구현체가 만들어져서 found에 최종대입되며, found를 통해서 외부IP를 가져온다든지, 포트매핑을 한다든지  할 것이다.

import (
.....
"github.com/huin/goupnp"
"github.com/huin/goupnp/dcps/internetgateway1"
"github.com/huin/goupnp/dcps/internetgateway2"
)

먼저 이더리움에서는 huin/goupnp 라이브러리를 사용하고 있다.

type upnp struct {
dev *goupnp.RootDevice
service string
client upnpClient
}

type upnpClient interface {
GetExternalIPAddress() (string, error)
AddPortMapping(string, uint16, string, uint16, string, bool, string, uint32) error
DeletePortMapping(string, uint16, string) error
GetNATRSIPStatus() (sip bool, nat bool, err error)
}

upnp 구조체에서 upnpClient 인터페이스를 보면 외부IP 주소를 가져오는 함수(GetExternalIPAddress) 와 포트를 매핑해주는 함수(AddPortMapping)가 있음을 알 수 있다.

upnpClient 를 통해서 값을 가져오고 매핑하는 코드를 보면 아래와 같다.

func (n *upnp) ExternalIP() (addr net.IP, err error) {
ipString, err := n.client.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, errors.New("bad IP in response")
}
return ip, nil
}

func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error {
ip, err := n.internalAddress()
if err != nil {
return nil
}
protocol = strings.ToUpper(protocol)
lifetimeS := uint32(lifetime / time.Second)
n.DeleteMapping(protocol, extport, intport)
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
}

이제 upnpClient를 세팅하고 upnp객체를 만드는 코드를 살펴보자.


// discoverUPnP searches for Internet Gateway Devices
// and returns the first one it can find on the local network.
func discoverUPnP() Interface {
found := make(chan *upnp, 2)
// IGDv1
go discover(found, internetgateway1.URN_WANConnectionDevice_1, func(dev *goupnp.RootDevice, sc goupnp.ServiceClient) *upnp {
switch sc.Service.ServiceType {
case internetgateway1.URN_WANIPConnection_1:
return &upnp{dev, "IGDv1-IP1", &internetgateway1.WANIPConnection1{ServiceClient: sc}}
case internetgateway1.URN_WANPPPConnection_1:
return &upnp{dev, "IGDv1-PPP1", &internetgateway1.WANPPPConnection1{ServiceClient: sc}}
}
return nil
})
// IGDv2
... 생략 ...

for i := 0; i < cap(found); i++ {
if c := <-found; c != nil {
return c
}
}
return nil
}

// finds devices matching the given target and calls matcher for all
// advertised services of each device. The first non-nil service found
// is sent into out. If no service matched, nil is sent.
func discover(out chan<- *upnp, target string, matcher func(*goupnp.RootDevice, goupnp.ServiceClient) *upnp) {
devs, err := goupnp.DiscoverDevices(target)
if err != nil {
out <- nil
return
}
found := false
for i := 0; i < len(devs) && !found; i++ {
if devs[i].Root == nil {
continue
}
devs[i].Root.Device.VisitServices(func(service *goupnp.Service) {
if found {
return
}
// check for a matching IGD service
sc := goupnp.ServiceClient{
SOAPClient: service.NewSOAPClient(),
RootDevice: devs[i].Root,
Location: devs[i].Location,
Service: service,
}
sc.SOAPClient.HTTPClient.Timeout = soapRequestTimeout
upnp := matcher(devs[i].Root, sc)
if upnp == nil {
return
}
// check whether port mapping is enabled
if _, nat, err := upnp.client.GetNATRSIPStatus(); err != nil || !nat {
return
}
out <- upnp
found = true
})
}
if !found {
out <- nil
}
}

꽤나 복잡해보이지만 목적은 단순하다. 아래의 구조체에 값을 채우는 것이다. 값을 채운후에 client를 통해 외부ip값도 가져올수 있고, 포트매핑도 할 수 있게 된다.

.type upnp struct {
   dev     *goupnp.RootDevice
   service string
   client  upnpClient
}

이렇게 만들어진 client upnpClient 를 통해서 외부ip값을 가져오고 매핑하는 코드를 보면 아래와 같다.

func (n *upnp) ExternalIP() (addr net.IP, err error) {
ipString, err := n.client.GetExternalIPAddress()
if err != nil {
return nil, err
}
ip := net.ParseIP(ipString)
if ip == nil {
return nil, errors.New("bad IP in response")
}
return ip, nil
}

func (n *upnp) AddMapping(protocol string, extport, intport int, desc string, lifetime time.Duration) error {
ip, err := n.internalAddress()
if err != nil {
return nil
}
protocol = strings.ToUpper(protocol)
lifetimeS := uint32(lifetime / time.Second)
n.DeleteMapping(protocol, extport, intport)
return n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS)
}

 ipString, err := n.client.GetExternalIPAddress() 로 외부ip를 가져오며,
n.client.AddPortMapping("", uint16(extport), protocol, uint16(intport), ip.String(), true, desc, lifetimeS) 로 포트매핑을 해주고 있다.


Comments