上一篇文章我們介紹了Peer收發消息的機制,它是以Peer之間建立TCP連接為前提的;本文將介紹Peer之間如何建立及維護TCP接連。節點之間可以直接建立連接,也可以通過代理(Proxy)連接;特別地,它們之間還可以通過洋蔥代理(Onion Proxy)建立TCP連接,節點也可以將自己隱藏在“暗網”中以洋蔥地址的(.onion address)的形式供其他節點連接。接下來,我們將通過代碼來分析這些連接方式是如何實現的。
btcd/connmgr包中的文件包括:
- connmanager.go: 處理建立新的連接、通知連接狀態、重連及斷開連接等主要邏輯;
- dynamicbanscore.go:實現了一個動態計分器,用于記錄Peer之間消息交換的頻率,當分數大于設定的門限時會主動斷開連接,這是為了防止類似于DDoS攻擊;
- seed.go: 負責將內置于全節點客戶端里的種子節點的地址解析成Bitcoin協議里定義的網絡地址;
- tor.go: 通過洋蔥代理建立連接的節點,需要在Tor網絡上的最后一跳,即退出節點(exit node)上進行DNS解析,然后將解析結果通過洋蔥代理返回給節點,tor.go主要實現了通過洋蔥代理進行DNS解析的SOCKS消息交換。需要注意的是,這里的DNS解析并不是解析洋蔥地址,而是解析公網上的域名或者hostname,解析洋蔥地址是不能成功而且無意義的。
- log.go: 提供logger初始化及設定logger等方法;
- doc.go: 包btcd/connmgr的doc文件;
- connmanager_test.go、dynamicbanscore_test.go: 定義了相應的Test方法;
通過代理或者洋蔥代理進行TCP連接的代碼位于btcsuite/go-socks(btcd項目的btcsuite/btcd/vendor/github.com/btcsuite/go-socks目錄),它實現了SOCKS 5協議的client部分,包含的文件有:
- addr.go: 定義了ProxiedAddr,用于描述代理的外部地址,包括網絡類型(如tcp),主機名或地址及端口號;
- conn.go: 定義了proxiedConn,用于描述被代理的連接,提供了讀、寫代理連接的方法等;
- dial.go: 實現了建立代理連接的邏輯;
雖然ConnMgr支持通過洋蔥代理與“明網”或者“暗網”中的節點連接,但本文暫不深入介紹Tor網絡相關的知識,我們將在后文《Bitcoin網絡與Tor網絡的匿名性討論》中詳細介紹。接下來,我們先分析btcd/connmgr來了解連接建立及管理的機制,然后分析btcsuite/go-socks來了解通過代理進行連接的過程。btcd/connmgr中的主要類型包括: ConnManager、Config和ConnReq,它們的定義如下:
//btcd/connmgr/connmanager.go
// ConnManager provides a manager to handle network connections.
type ConnManager struct {
// The following variables must only be used atomically.
connReqCount uint64
start int32
stop int32
cfg Config
wg sync.WaitGroup
failedAttempts uint64
requests chan interface{}
quit chan struct{}
}
各字段的意義如下:
- connReqCount: 記錄主動連接其他節點的連接數量;
- start: 標識connmgr已經啟動;
- stop: 標識connmgr已經結束;
- cfg: 設定相關的配置,在Config的定義中介紹;
- wg: 用于同步connmgr的退出狀態,調用方可以阻塞等待connmgr的工作協程退出;
- failedAttempts: 某個連接失敗后,ConnMgr嘗試選擇新的Peer地址連接的總次數;
- requests:用于與connmgr工作協程通信的管道;
- quit: 用于通知工作協程退出;
ConnManager依賴于Config:
//btcd/connmgr/connmanager.go
// Config holds the configuration options related to the connection manager.
type Config struct {
// Listeners defines a slice of listeners for which the connection
// manager will take ownership of and accept connections. When a
// connection is accepted, the OnAccept handler will be invoked with the
// connection. Since the connection manager takes ownership of these
// listeners, they will be closed when the connection manager is
// stopped.
//
// This field will not have any effect if the OnAccept field is not
// also specified. It may be nil if the caller does not wish to listen
// for incoming connections.
Listeners []net.Listener
// OnAccept is a callback that is fired when an inbound connection is
// accepted. It is the caller's responsibility to close the connection.
// Failure to close the connection will result in the connection manager
// believing the connection is still active and thus have undesirable
// side effects such as still counting toward maximum connection limits.
//
// This field will not have any effect if the Listeners field is not
// also specified since there couldn't possibly be any accepted
// connections in that case.
OnAccept func(net.Conn)
// TargetOutbound is the number of outbound network connections to
// maintain. Defaults to 8.
TargetOutbound uint32
// RetryDuration is the duration to wait before retrying connection
// requests. Defaults to 5s.
RetryDuration time.Duration
// OnConnection is a callback that is fired when a new outbound
// connection is established.
OnConnection func(*ConnReq, net.Conn)
// OnDisconnection is a callback that is fired when an outbound
// connection is disconnected.
OnDisconnection func(*ConnReq)
// GetNewAddress is a way to get an address to make a network connection
// to. If nil, no new connections will be made automatically.
GetNewAddress func() (net.Addr, error)
// Dial connects to the address on the named network. It cannot be nil.
Dial func(net.Addr) (net.Conn, error)
}
各字段意義如下:
- Listeners: 節點上所有等待外部連接的監聽點;
- OnAccept: 節點應答并接受外部連接后的回調函數;
- TargetOutbound:節點主動向外連接Peer的最大個數;
- RetryDuration: 連接失敗后發起重連的等待時間,默認為5s,默認的最大重連等待時間為5min;
- OnConnection: 連接建立成功后的回調函數;
- OnDisconnection: 連接關閉后的回調函數;
- GetNewAddress: 連接失敗后,ConnMgr可能會選擇新的Peer進行連接,GetNewAddress函數提供獲取新Peer地址的方法,它最終會調用addrManager的GetAddress()來分配新地址,我們將在介紹addrmgr時詳細介紹;
- Dial: 定義建立TCP連接的方式,是直連還是通過代理連接;
ConnReq描述了一個連接,它的定義如下:
//btcd/connmgr/connmanager.go
// ConnReq is the connection request to a network address. If permanent, the
// connection will be retried on disconnection.
type ConnReq struct {
// The following variables must only be used atomically.
id uint64
Addr net.Addr
Permanent bool
conn net.Conn
state ConnState
stateMtx sync.RWMutex
retryCount uint32
}
- id: 連接的序號,用于索引;
- Addr: 連接的目的地址;
- Permanent: 標識是否與Peer保持永久連接,如果為true,則連接失敗后,繼續嘗試與該Peer連接,而不是選擇新的Peer地址重新連接;
- conn: 連接成功后,真實的net.Conn對象;
- state: 連接的狀態,有ConnPending、ConnEstablished、ConnDisconnected及ConnFailed等;
- stateMtx: 保護state狀態的讀寫鎖;
- retryCount: 如果Permanent為true,retryCount記錄該連接重復重連的次數;
我們先從ConnManager的Start()方法入手來分析它的工作機制:
//btcd/connmgr/connmanager.go
// Start launches the connection manager and begins connecting to the network.
func (cm *ConnManager) Start() {
// Already started?
if atomic.AddInt32(&cm.start, 1) != 1 {
return
}
log.Trace("Connection manager started")
cm.wg.Add(1)
go cm.connHandler() (1)
// Start all the listeners so long as the caller requested them and
// provided a callback to be invoked when connections are accepted.
if cm.cfg.OnAccept != nil {
for _, listner := range cm.cfg.Listeners {
cm.wg.Add(1)
go cm.listenHandler(listner) (2)
}
}
for i := atomic.LoadUint64(&cm.connReqCount); i < uint64(cm.cfg.TargetOutbound); i++ {
go cm.NewConnReq() (3)
}
}
可以看出,ConnMgr啟動時主要有如下過程:
- 啟動工作協程connHandler;
- 啟動監聽協程listenHandler,等待其他節點連接;
- 啟動建立連接的協程,選擇Peer地址并主動連接;
ConnMgr中各協程及其通信的channel示意如下圖所示:
其中caller是指調用協程,onConnect、OnDisconnect和OnAccept均在新的協程中回調,以免阻塞ConnMgr的工作協程和監聽協程。在開始分析上述三個協程之前,我們先來看看Connect()和Disconnect()方法了解建立和斷開連接的實現:
//btcd/connmgr/connmanager.go
// Connect assigns an id and dials a connection to the address of the
// connection request.
func (cm *ConnManager) Connect(c *ConnReq) {
......
conn, err := cm.cfg.Dial(c.Addr)
if err != nil {
cm.requests <- handleFailed{c, err}
} else {
cm.requests <- handleConnected{c, conn}
}
}
// Disconnect disconnects the connection corresponding to the given connection
// id. If permanent, the connection will be retried with an increasing backoff
// duration.
func (cm *ConnManager) Disconnect(id uint64) {
if atomic.LoadInt32(&cm.stop) != 0 {
return
}
cm.requests <- handleDisconnected{id, true}
}
可以看出,建立連接的過程就是調用指定的Dial()方法來進行TCP握手,如果與Peer直連(指不經過代理),則直接調用net.Dial()進行連接;如果通過代理與Peer連接,則會調用SOCKS Proxy的Dial()方法,我們將在分析go-socks中看到。然后,根據是否連接成功向connHandler發送成功或者失敗的消息,讓connHandler進一步處理。調用Disconnect斷開連接則向connHandler發送handleDisconnected消息讓connHandler進一步處理??磥?,連接或者斷開連接的主要處理邏輯在connHandler中,我們來看看它的實現:
//btcd/connmgr/connmanager.go
// connHandler handles all connection related requests. It must be run as a
// goroutine.
//
// The connection handler makes sure that we maintain a pool of active outbound
// connections so that we remain connected to the network. Connection requests
// are processed and mapped by their assigned ids.
func (cm *ConnManager) connHandler() {
conns := make(map[uint64]*ConnReq, cm.cfg.TargetOutbound)
out:
for {
select {
case req := <-cm.requests:
switch msg := req.(type) {
case handleConnected:
connReq := msg.c
connReq.updateState(ConnEstablished)
connReq.conn = msg.conn
conns[connReq.id] = connReq
log.Debugf("Connected to %v", connReq)
connReq.retryCount = 0
cm.failedAttempts = 0
if cm.cfg.OnConnection != nil {
go cm.cfg.OnConnection(connReq, msg.conn)
}
case handleDisconnected:
if connReq, ok := conns[msg.id]; ok {
connReq.updateState(ConnDisconnected)
if connReq.conn != nil {
connReq.conn.Close()
}
log.Debugf("Disconnected from %v", connReq)
delete(conns, msg.id)
if cm.cfg.OnDisconnection != nil {
go cm.cfg.OnDisconnection(connReq)
}
if uint32(len(conns)) < cm.cfg.TargetOutbound && msg.retry {
cm.handleFailedConn(connReq)
}
} else {
log.Errorf("Unknown connection: %d", msg.id)
}
case handleFailed:
connReq := msg.c
connReq.updateState(ConnFailed)
log.Debugf("Failed to connect to %v: %v", connReq, msg.err)
cm.handleFailedConn(connReq)
}
case <-cm.quit:
break out
}
}
cm.wg.Done()
log.Trace("Connection handler done")
}
connHandler主要處理連接建立成功、失敗和斷連這三種情況:
- 如果連接成功,首先更新連接的狀態為ConnEstablished,同時將該連接添加到conns中以跟蹤它的后續狀態,并將retryCount和failedAttempts重置,隨后在新的goroutine中回調OnConnection;
- 如果要斷開連接,先從conns找到要斷開的connReq,更新連接狀態為ConnDisconnected,調用net.Conn的Close()方法斷開TCP連接,隨后在新的goroutine中回調OnDisconnection;最后,如果是當前的活躍連接數少于設定的最大門限且retry設為true,則調用handleFailedConn進行重連或者選擇新的Peer連接;
- 如果連接失敗,則將連接狀態更新為ConnFailed,同時調用handleFailedConn進行重連或者選擇新的Peer連接;
需要注意的是,ConnMgr只處理了連接建立成功或者失敗的情況,并沒有專門處理連接成功一段時間后連接中斷的情況,這是因為TCP socket雖然有keepalive選項開啟心跳,但并沒有心跳超時的回調,只有當調用write()方法寫入數據返回錯誤時才能檢測到連接中斷,所以一般需要應用層協議通過心跳的方式檢測網絡中斷的情形。我們在《Btcd區塊在P2P網絡上的傳播之Peer》中介紹過,Peer之間會發送Ping/Pong心跳來維持及檢測連接。如果Pong消息超時或者outHandler向net.Conn寫數據出錯時,Peer的Disconnect()方法會被調用以主動斷開連接,并退出Peer的工作協程。當Peer連接建立成功并回調OnConnect()時,server會新起一個goroutine守護與Peer的連接狀態;當Peer斷連并退出時,server隨即會調用ConnMgr的Disconnect()方法以清除該連接。
接下來,我們看看handleFailedConn的實現:
//btcd/connmgr/connmanager.go
// handleFailedConn handles a connection failed due to a disconnect or any
// other failure. If permanent, it retries the connection after the configured
// retry duration. Otherwise, if required, it makes a new connection request.
// After maxFailedConnectionAttempts new connections will be retried after the
// configured retry duration.
func (cm *ConnManager) handleFailedConn(c *ConnReq) {
if atomic.LoadInt32(&cm.stop) != 0 {
return
}
if c.Permanent {
c.retryCount++
d := time.Duration(c.retryCount) * cm.cfg.RetryDuration
if d > maxRetryDuration {
d = maxRetryDuration
}
log.Debugf("Retrying connection to %v in %v", c, d)
time.AfterFunc(d, func() {
cm.Connect(c)
})
} else if cm.cfg.GetNewAddress != nil {
cm.failedAttempts++
if cm.failedAttempts >= maxFailedAttempts {
......
time.AfterFunc(cm.cfg.RetryDuration, func() {
cm.NewConnReq()
})
} else {
go cm.NewConnReq()()
}
}
}
handleFailedConn主要處理重連邏輯,它的主要思想為:
- 如果連接的Permanent為true,即該連接為“持久”連接,連接失敗進需要重連;需要注意的時,重連的等待時間是與重連的次數成正比的,即第1次重連需等待5s,第2次重連需要等待10s,以次類推,最大等待時間為5min;
- 如果連接不是“持久”連接,則選擇新的Peer進行連接,如果嘗試新連接的次數超限(默認為25次),則表明節點的出口網絡可能斷連,需要延時連接,默認延時5s;
動態選擇Peer并發起連接的過程在NewConnReq()中實現:
//btcd/connmgr/connmanager.go
/ NewConnReq creates a new connection request and connects to the
// corresponding address.
func (cm *ConnManager) NewConnReq() {
......
c := &ConnReq{}
atomic.StoreUint64(&c.id, atomic.AddUint64(&cm.connReqCount, 1))
addr, err := cm.cfg.GetNewAddress()
if err != nil {
cm.requests <- handleFailed{c, err}
return
}
c.Addr = addr
cm.Connect(c)
}
其主要過程為:
- 新建ConnReq對象,并為其分配一個id;
- 通過GetNewAddress()從addrmgr維護的地址倉庫中隨機選擇一個Peer的可達地址,如果地址選擇失敗,則由connHandler再次發起新的連接;
- 調用Connect()方法開始與Peer建立連接;
上面各方法已經展示了ConnMgr主動與Peer建立連接,及失敗后重連或者選擇新地址連接的過程,接下來,我們通過listenHandler來看它被動等待連接的實現:
//btcd/connmgr/connmanager.go
// listenHandler accepts incoming connections on a given listener. It must be
// run as a goroutine.
func (cm *ConnManager) listenHandler(listener net.Listener) {
log.Infof("Server listening on %s", listener.Addr())
for atomic.LoadInt32(&cm.stop) == 0 {
conn, err := listener.Accept()
if err != nil {
// Only log the error if not forcibly shutting down.
if atomic.LoadInt32(&cm.stop) == 0 {
log.Errorf("Can't accept connection: %v", err)
}
continue
}
go cm.cfg.OnAccept(conn)
}
cm.wg.Done()
log.Tracef("Listener handler done for %s", listener.Addr())
}
可以看出,listenHandler主要是等待連接,連接成功后在新協程中回調OnAccept。實際上,OnConnect和OnAccept回調將在server中實現,而是創建Peer并調用Peer的AssociateConnection()方法的入口,我們將在分析server.go中詳細介紹。
以上就是ConnMgr建立及維護連接的主要過程。接下來,我們來分析用于防止DDoS攻擊的動態計分器是如何實現的,先看DynamicBanScore的定義:
//btcd/connmgr/dynamicbanscore.go
// DynamicBanScore provides dynamic ban scores consisting of a persistent and a
// decaying component. The persistent score could be utilized to create simple
// additive banning policies similar to those found in other bitcoin node
// implementations.
//
// The decaying score enables the creation of evasive logic which handles
// misbehaving peers (especially application layer DoS attacks) gracefully
// by disconnecting and banning peers attempting various kinds of flooding.
// DynamicBanScore allows these two approaches to be used in tandem.
//
// Zero value: Values of type DynamicBanScore are immediately ready for use upon
// declaration.
type DynamicBanScore struct {
lastUnix int64
transient float64
persistent uint32
mtx sync.Mutex
}
其各字段意義如下:
- lastUnix: 上一次調整分值的Unix時間點;
- transient: 分值的浮動衰減部分;
- persistent: 分值中不會自動衰減的部分;
- mtx: 保護transient和persistent的互斥鎖;
從上面的定義看,DynamicBanScore提供的分值是由一個不變值和瞬時值構成的,那么這兩值到底是如何起作用的呢,我們可以看看它的int()方法:
//btcd/connmgr/dynamicbanscore.go
// int returns the ban score, the sum of the persistent and decaying scores at a
// given point in time.
//
// This function is not safe for concurrent access. It is intended to be used
// internally and during testing.
func (s *DynamicBanScore) int(t time.Time) uint32 {
dt := t.Unix() - s.lastUnix
if s.transient < 1 || dt < 0 || Lifetime < dt {
return s.persistent
}
return s.persistent + uint32(s.transient*decayFactor(dt))
}
可以看出,最后的分值等于persistent加上transient乘以一個衰減系數后的和。其中衰減系數隨時間變化,它由decayFactor()決定:
//btcd/connmgr/dynamicbanscore.go
// decayFactor returns the decay factor at t seconds, using precalculated values
// if available, or calculating the factor if needed.
func decayFactor(t int64) float64 {
if t < precomputedLen {
return precomputedFactor[t]
}
return math.Exp(-1.0 * float64(t) * lambda)
}
可以看出,衰減系數是按時間間隔呈指數分布的,其中Lambda=ln2/60。動態分值隨時間時隔變化的曲線如下圖所示:
這里的時間間隔是指當前取值時刻距上一次主動調節persistent或者transistent值的時間差。
//btcd/connmgr/dynamicbanscore.go
// increase increases the persistent, the decaying or both scores by the values
// passed as parameters. The resulting score is calculated as if the action was
// carried out at the point time represented by the third parameter. The
// resulting score is returned.
//
// This function is not safe for concurrent access.
func (s *DynamicBanScore) increase(persistent, transient uint32, t time.Time) uint32 {
s.persistent += persistent
tu := t.Unix()
dt := tu - s.lastUnix
if transient > 0 {
if Lifetime < dt {
s.transient = 0
} else if s.transient > 1 && dt > 0 {
s.transient *= decayFactor(dt)
}
s.transient += float64(transient)
s.lastUnix = tu
}
return s.persistent + uint32(s.transient)
}
可以看出,主動調節score值時,先將persistent值直接相加,然后算出傳入時刻t的transient值,再與傳入的transient值相加后得到新的transient值,新的persistent與新的transient值相加后得到新的score。實際上,就是t時刻的score加上傳入的persistent和transient即得到新的score。
Peer之間交換消息時,每一個Peer連接會有一個動態計分器來監控它們之間收發消息的頻率,太頻繁地收到某個Peer發過來的消息時,將被懷疑遭到DDoS攻擊,從而主動斷開與它的連接,我們將在分析協議消息的收發時看到這一點。
通過前面的分析,我們知道ConnMgr會通過GetNewAddress()來選取Peer的地址,但一個新的節點接入時,它還沒有與任何Peer交換過地址信息,所以它的地址倉庫是空的,那它該與哪些節點先建立連接呢?實際上,節點會內置一些種子節點的地址:
//btcd/chaincfg/params.go
// MainNetParams defines the network parameters for the main Bitcoin network.
var MainNetParams = Params{
Name: "mainnet",
Net: wire.MainNet,
DefaultPort: "8333",
DNSSeeds: []DNSSeed{
{"seed.bitcoin.sipa.be", true},
{"dnsseed.bluematt.me", true},
{"dnsseed.bitcoin.dashjr.org", false},
{"seed.bitcoinstats.com", true},
{"seed.bitnodes.io", false},
{"seed.bitcoin.jonasschnelli.ch", true},
},
......
}
Btcd節點內置了如上6個種子節點的域名。然而,在ConnMgr連接種子節點之前,必須進行DNS Lookup查詢它們對應的IP地址,這是在SeedFromDNS()中完成的:
//btcd/connmgr/seed.go
// SeedFromDNS uses DNS seeding to populate the address manager with peers.
func SeedFromDNS(chainParams *chaincfg.Params, reqServices wire.ServiceFlag,
lookupFn LookupFunc, seedFn OnSeed) {
for _, dnsseed := range chainParams.DNSSeeds {
var host string
if !dnsseed.HasFiltering || reqServices == wire.SFNodeNetwork {
host = dnsseed.Host
} else {
host = fmt.Sprintf("x%x.%s", uint64(reqServices), dnsseed.Host)
}
go func(host string) {
randSource := mrand.New(mrand.NewSource(time.Now().UnixNano()))
seedpeers, err := lookupFn(host) (1)
if err != nil {
log.Infof("DNS discovery failed on seed %s: %v", host, err)
return
}
numPeers := len(seedpeers)
log.Infof("%d addresses found from DNS seed %s", numPeers, host)
if numPeers == 0 {
return
}
addresses := make([]*wire.NetAddress, len(seedpeers))
// if this errors then we have *real* problems
intPort, _ := strconv.Atoi(chainParams.DefaultPort)
for i, peer := range seedpeers {
addresses[i] = wire.NewNetAddressTimestamp( (2)
// bitcoind seeds with addresses from
// a time randomly selected between 3
// and 7 days ago.
time.Now().Add(-1*time.Second*time.Duration(secondsIn3Days+
randSource.Int31n(secondsIn4Days))),
0, peer, uint16(intPort))
}
seedFn(addresses)
}(host)
}
}
它的主要步驟為:
- 調用lookupFn()進行DNS resolve,將種子節點的域名解析了IP地址;
- 將種子節點的IP地址封裝為協議地址wire.NetAddress,其中主要是增加了地址的時效性,這里將地址的時效隨機地設為3到7天。
這里傳入的lookupFn()根據配置,有可能是節點自己訪問DNS Server解析,也有可能通過洋蔥代理進行解析:
//btcd/config.go
func loadConfig() (*config, []string, error) {
......
// Setup dial and DNS resolution (lookup) functions depending on the
// specified options. The default is to use the standard
// net.DialTimeout function as well as the system DNS resolver. When a
// proxy is specified, the dial function is set to the proxy specific
// dial function and the lookup is set to use tor (unless --noonion is
// specified in which case the system DNS resolver is used).
cfg.dial = net.DialTimeout
cfg.lookup = net.LookupIP
if cfg.Proxy != "" {
_, _, err := net.SplitHostPort(cfg.Proxy)
......
// Tor isolation flag means proxy credentials will be overridden
// unless there is also an onion proxy configured in which case
// that one will be overridden.
torIsolation := false
if cfg.TorIsolation && cfg.OnionProxy == "" &&
(cfg.ProxyUser != "" || cfg.ProxyPass != "") {
torIsolation = true
fmt.Fprintln(os.Stderr, "Tor isolation set -- "+
"overriding specified proxy user credentials")
}
proxy := &socks.Proxy{
Addr: cfg.Proxy,
Username: cfg.ProxyUser,
Password: cfg.ProxyPass,
TorIsolation: torIsolation,
}
cfg.dial = proxy.DialTimeout
// Treat the proxy as tor and perform DNS resolution through it
// unless the --noonion flag is set or there is an
// onion-specific proxy configured.
if !cfg.NoOnion && cfg.OnionProxy == "" {
cfg.lookup = func(host string) ([]net.IP, error) {
return connmgr.TorLookupIP(host, cfg.Proxy)
}
}
}
// Setup onion address dial function depending on the specified options.
// The default is to use the same dial function selected above. However,
// when an onion-specific proxy is specified, the onion address dial
// function is set to use the onion-specific proxy while leaving the
// normal dial function as selected above. This allows .onion address
// traffic to be routed through a different proxy than normal traffic.
if cfg.OnionProxy != "" {
_, _, err := net.SplitHostPort(cfg.OnionProxy)
......
cfg.oniondial = func(network, addr string, timeout time.Duration) (net.Conn, error) {
proxy := &socks.Proxy{
Addr: cfg.OnionProxy,
Username: cfg.OnionProxyUser,
Password: cfg.OnionProxyPass,
TorIsolation: cfg.TorIsolation,
}
return proxy.DialTimeout(network, addr, timeout)
}
// When configured in bridge mode (both --onion and --proxy are
// configured), it means that the proxy configured by --proxy is
// not a tor proxy, so override the DNS resolution to use the
// onion-specific proxy.
if cfg.Proxy != "" {
cfg.lookup = func(host string) ([]net.IP, error) {
return connmgr.TorLookupIP(host, cfg.OnionProxy)
}
}
} else {
cfg.oniondial = cfg.dial
}
// Specifying --noonion means the onion address dial function results in
// an error.
if cfg.NoOnion {
cfg.oniondial = func(a, b string, t time.Duration) (net.Conn, error) {
return nil, errors.New("tor has been disabled")
}
}
......
}
從上述代碼可以看出:
- 默認的DNS Lookup和Dial方法就是標準的net.LookupIP和net.DialTimeout;
- 如果設置了代理,Dial方法將使用SOCKS Proxy的DialTimeout(),如果未禁用洋蔥代理,則默認代理為洋蔥代理,DNS查詢將通過connmgr的TorLookupIP()實現;
- 如果專門設置了洋蔥代理,則設定對“暗網”服務(hidden service)的連接采用SOCKS Proxy的DialTimeout(),DNS Lookup將使用connmgr的TorLookupIP();請注意,即使設置了洋蔥代理,對“明網”地址的連接仍是根據是否設置了普通SOCKS代理(非Tor代理)來決定采用標準的net.DialTimeout還是Proxy的DialTimeout;
無論是通過普通代理還是洋蔥代理連接Peer,對節點來講,它們均是SOCKS代理服務器,節點與它們之間通過SOCKS協議來通信。與普通代理相比,洋蔥代理擴展了SOCKS協議,加入了對Name lookup、Stream Isolation等的支持。SOCKS協議位于會話層,在傳輸層與應用層之間,所以它不僅可以代理HTTP流量,也可以代理如FTP、XMPP等等的其他應用流量。SOCKS協議比較簡單,我們不再展開介紹,讀者可以閱讀RFC1928及RFC1929來了解它的消息格式。為了了解Btcd如何通過SOCKS代理建立連接,我們來看看Proxy的dial()方法:
//btcd/vendor/github.com/btcsuite/go-socks/dial.go
func (p *Proxy) dial(network, addr string, timeout time.Duration) (net.Conn, error) {
host, strPort, err := net.SplitHostPort(addr)
if err != nil {
return nil, err
}
port, err := strconv.Atoi(strPort)
if err != nil {
return nil, err
}
conn, err := net.DialTimeout("tcp", p.Addr, timeout) (1)
if err != nil {
return nil, err
}
var user, pass string
if p.TorIsolation { (2)
var b [16]byte
_, err := io.ReadFull(rand.Reader, b[:])
if err != nil {
conn.Close()
return nil, err
}
user = hex.EncodeToString(b[0:8])
pass = hex.EncodeToString(b[8:16])
} else {
user = p.Username
pass = p.Password
}
buf := make([]byte, 32+len(host)+len(user)+len(pass))
// Initial greeting
buf[0] = protocolVersion (3)
if user != "" {
buf = buf[:4]
buf[1] = 2 // num auth methods
buf[2] = authNone
buf[3] = authUsernamePassword
} else {
buf = buf[:3]
buf[1] = 1 // num auth methods
buf[2] = authNone
}
_, err = conn.Write(buf)
if err != nil {
conn.Close()
return nil, err
}
// Server's auth choice
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
conn.Close()
return nil, err
}
if buf[0] != protocolVersion {
conn.Close()
return nil, ErrInvalidProxyResponse
}
err = nil
switch buf[1] {
default:
err = ErrInvalidProxyResponse
case authUnavailable:
err = ErrNoAcceptableAuthMethod
case authGssApi:
err = ErrNoAcceptableAuthMethod
case authUsernamePassword:
buf = buf[:3+len(user)+len(pass)] (4)
buf[0] = 1 // version
buf[1] = byte(len(user))
copy(buf[2:], user)
buf[2+len(user)] = byte(len(pass))
copy(buf[3+len(user):], pass)
if _, err = conn.Write(buf); err != nil {
conn.Close()
return nil, err
}
if _, err = io.ReadFull(conn, buf[:2]); err != nil {
conn.Close()
return nil, err
}
if buf[0] != 1 { // version
err = ErrInvalidProxyResponse
} else if buf[1] != 0 { // 0 = succes, else auth failed
err = ErrAuthFailed
}
case authNone:
// Do nothing
}
if err != nil {
conn.Close()
return nil, err
}
// Command / connection request
buf = buf[:7+len(host)] (5)
buf[0] = protocolVersion
buf[1] = commandTcpConnect
buf[2] = 0 // reserved
buf[3] = addressTypeDomain
buf[4] = byte(len(host))
copy(buf[5:], host)
buf[5+len(host)] = byte(port >> 8)
buf[6+len(host)] = byte(port & 0xff)
if _, err := conn.Write(buf); err != nil {
conn.Close()
return nil, err
}
// Server response
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
conn.Close()
return nil, err
}
if buf[0] != protocolVersion {
conn.Close()
return nil, ErrInvalidProxyResponse
}
if buf[1] != statusRequestGranted {
conn.Close()
err := statusErrors[buf[1]]
if err == nil {
err = ErrInvalidProxyResponse
}
return nil, err
}
paddr := &ProxiedAddr{Net: network}
switch buf[3] { (6)
default:
conn.Close()
return nil, ErrInvalidProxyResponse
case addressTypeIPv4:
if _, err := io.ReadFull(conn, buf[:4]); err != nil {
conn.Close()
return nil, err
}
paddr.Host = net.IP(buf).String()
case addressTypeIPv6:
if _, err := io.ReadFull(conn, buf[:16]); err != nil {
conn.Close()
return nil, err
}
paddr.Host = net.IP(buf).String()
case addressTypeDomain:
if _, err := io.ReadFull(conn, buf[:1]); err != nil {
conn.Close()
return nil, err
}
domainLen := buf[0]
if _, err := io.ReadFull(conn, buf[:domainLen]); err != nil {
conn.Close()
return nil, err
}
paddr.Host = string(buf[:domainLen])
}
if _, err := io.ReadFull(conn, buf[:2]); err != nil {
conn.Close()
return nil, err
}
paddr.Port = int(buf[0])<<8 | int(buf[1])
return &proxiedConn{ (7)
conn: conn,
boundAddr: paddr,
remoteAddr: &ProxiedAddr{network, host, port},
}, nil
}
由于Btcd節點之間均通過TCP連接,因此這里實現的是SOCKS代理TCP連接的情形。建立代理連接的主要步驟為:
- 與SOCKS代理服務器建立TCP連接,如代碼(1)處所示;
- 客戶端向代理服務器發送協議版本和METHOD集合的協商請求,如代碼(3)處所示,客戶端選擇版本5,選擇的認證方法為不驗證或者用戶名/密碼驗證,或者僅僅是不認證;
- 然后等待SOCKS服務器響應。如果SOCKS服務器不支持SOCKS 5,則協商失??;如果SOCKS服務器支持SOCKS 5,并同意不驗證,則客戶端可以直接發送后續請求,如果SOCKS服務器指定采用用戶名/密碼認證,則客戶端隨后向服務器提交用戶名和密碼,服務器將驗證并返回結果,如代碼(4)所示;
- 無需要認證或者用戶名/密碼驗證通過后,客戶端向SOCKS服務器發送CONNECT請求,并指明目的IP和端口號,如代碼(5)處所示;
- SOCKS服務器響應CONNECT請求,如果代理連接成功,則返回外部的代理地址和端口。根據響應消息中指明的代理地址類型,代理地址可能是IPv4、IPv6或者Domain Name。
- 創建并返回一個代理連接對象proxiedConn,它的conn字段描述客戶端與SOCKS服務器的TCP連接,該連接上的TCP報文將通過代理服務器轉發給目的地址,boundAddr描述代理的外部地址和端口,remoteAddr描述目的地址與端口。
特別地,如果客戶端連接一個Tor代理,并且希望開啟Stream Isolation特性,則隨機生成用戶名和密碼并發往Tor代理服務器。Stream Isolation是為了禁止Tor網絡在同一個“虛電路”上中繼不同的TCP流,Tor代理服務器支持通過IsolateClientAddr、IsolateSOCKSAuth、IsolateClientProtocol、IsolateDestPort及IsolateDestAddr等方式來標識不同的TCP流。Btcd選擇通過IsolateSOCKSAuth來支持Stream Isolation,使得同一節點在連接不同Peer或者重連相同Peer時的TCP在Tor網絡中均能被“隔離”。然而,讀者可能會產生疑問: 隨機生成的用戶名和密碼如何被Tor代理服務器驗證?實際上,Btcd這里使用隨機用戶名和密碼,是要求Tor代理服務器作如下配置: 選擇“NO AUTHENTICATION REQUIRED”作為驗證方式,并且只通過username來標識不同代理請求。
了解了通過SOCKS代理或者Tor代理與Peer建立TCP連接的機制后,我們就可以來看看如何通過Tor代理來進行DNS查詢。再次強調一下,通過Tor代理進行DNS查詢不是解析洋蔥地址,而是解析“明網”中的域名。例如,用戶通過Tor代理訪問www.google.com時,用戶可以選擇先通過DNS查詢到IP地址后,再通過Tor代理連接該IP地址;也可以將該域名作為目的地址發給Tor代理,讓Tor網絡的退出結點進行DNS查詢,并建立與目的地址的連接。如果某些客戶端不希望向DNS Server暴露自己的目標訪問域名,同時又希望進行域名解析,那它可以通過Tor代理進行DNS解析。
//btcd/connmgr/tor.go
// TorLookupIP uses Tor to resolve DNS via the SOCKS extension they provide for
// resolution over the Tor network. Tor itself doesn't support ipv6 so this
// doesn't either.
func TorLookupIP(host, proxy string) ([]net.IP, error) {
conn, err := net.Dial("tcp", proxy)
if err != nil {
return nil, err
}
defer conn.Close()
buf := []byte{'\x05', '\x01', '\x00'} (1)
_, err = conn.Write(buf)
if err != nil {
return nil, err
}
buf = make([]byte, 2)
_, err = conn.Read(buf)
if err != nil {
return nil, err
}
if buf[0] != '\x05' {
return nil, ErrTorInvalidProxyResponse
}
if buf[1] != '\x00' {
return nil, ErrTorUnrecognizedAuthMethod
}
buf = make([]byte, 7+len(host))
buf[0] = 5 // protocol version
buf[1] = '\xF0' // Tor Resolve (2)
buf[2] = 0 // reserved
buf[3] = 3 // Tor Resolve
buf[4] = byte(len(host))
copy(buf[5:], host)
buf[5+len(host)] = 0 // Port 0
_, err = conn.Write(buf)
if err != nil {
return nil, err
}
buf = make([]byte, 4)
_, err = conn.Read(buf)
if err != nil {
return nil, err
}
if buf[0] != 5 {
return nil, ErrTorInvalidProxyResponse
}
if buf[1] != 0 {
if int(buf[1]) >= len(torStatusErrors) {
return nil, ErrTorInvalidProxyResponse
} else if err := torStatusErrors[buf[1]]; err != nil {
return nil, err
}
return nil, ErrTorInvalidProxyResponse
}
if buf[3] != 1 { (3)
err := torStatusErrors[torGeneralError]
return nil, err
}
buf = make([]byte, 4)
bytes, err := conn.Read(buf)
if err != nil {
return nil, err
}
if bytes != 4 {
return nil, ErrTorInvalidAddressResponse
}
r := binary.BigEndian.Uint32(buf)
addr := make([]net.IP, 1)
addr[0] = net.IPv4(byte(r>>24), byte(r>>16), byte(r>>8), byte(r))
return addr, nil
}
其過程與建立代理連接的方程類似,即先協商版本與認證方式,再發送請求與等待響應。不同的地方在于:
- 選擇不認證的方式,如代碼(1)處所示;
- 請求的命令是'FO',它是Tor代理擴展的命令,指明用于Name Lookup,同時目標地址類型指定為DOMAINNAME,如代碼(2)處所示;
- Tor退出節點進行DNS查詢后,由Tor代碼返回。這里僅接受IPv4地址,如代碼(3)處所示;
到此,我們就完整分析了Bitcoin P2P網絡中Peer節點之間建立、維持和斷開TCP連接的所有過程,包括了通過SOCKS代理或Tor代理進行連接或DNS查詢的實現。然而,我們也了解到,除了節點內置的種子節點的地址,節點接入網絡時并不知道其他節點的地址,那么節點是如何知道網絡中其他節點的地址,以及如何選擇Peer節點地址建立連接呢?我們將在《Btcd區塊在P2P網絡上的傳播之AddrManager》中分析。由于本文涉及到了Tor網絡,有些讀者可能希望進一步了解Tor,同時,Bitcoin網絡與Tor網絡均做到了對源或者賬戶匿名,所以我們在分析AddrManager之前,下一篇文章將討論Bitcoin網絡與Tor網絡匿名性。