Btcd區(qū)塊在P2P網(wǎng)絡(luò)上的傳播之ConnMgr

上一篇文章我們介紹了Peer收發(fā)消息的機(jī)制讲竿,它是以Peer之間建立TCP連接為前提的;本文將介紹Peer之間如何建立及維護(hù)TCP接連。節(jié)點(diǎn)之間可以直接建立連接浊闪,也可以通過代理(Proxy)連接删铃;特別地,它們之間還可以通過洋蔥代理(Onion Proxy)建立TCP連接攒射,節(jié)點(diǎn)也可以將自己隱藏在“暗網(wǎng)”中以洋蔥地址的(.onion address)的形式供其他節(jié)點(diǎn)連接醋旦。接下來,我們將通過代碼來分析這些連接方式是如何實(shí)現(xiàn)的会放。

btcd/connmgr包中的文件包括:

  • connmanager.go: 處理建立新的連接饲齐、通知連接狀態(tài)、重連及斷開連接等主要邏輯;
  • dynamicbanscore.go:實(shí)現(xiàn)了一個(gè)動(dòng)態(tài)計(jì)分器咧最,用于記錄Peer之間消息交換的頻率捂人,當(dāng)分?jǐn)?shù)大于設(shè)定的門限時(shí)會(huì)主動(dòng)斷開連接,這是為了防止類似于DDoS攻擊;
  • seed.go: 負(fù)責(zé)將內(nèi)置于全節(jié)點(diǎn)客戶端里的種子節(jié)點(diǎn)的地址解析成Bitcoin協(xié)議里定義的網(wǎng)絡(luò)地址;
  • tor.go: 通過洋蔥代理建立連接的節(jié)點(diǎn)矢沿,需要在Tor網(wǎng)絡(luò)上的最后一跳滥搭,即退出節(jié)點(diǎn)(exit node)上進(jìn)行DNS解析,然后將解析結(jié)果通過洋蔥代理返回給節(jié)點(diǎn)捣鲸,tor.go主要實(shí)現(xiàn)了通過洋蔥代理進(jìn)行DNS解析的SOCKS消息交換瑟匆。需要注意的是,這里的DNS解析并不是解析洋蔥地址栽惶,而是解析公網(wǎng)上的域名或者h(yuǎn)ostname愁溜,解析洋蔥地址是不能成功而且無意義的。
  • log.go: 提供logger初始化及設(shè)定logger等方法;
  • doc.go: 包btcd/connmgr的doc文件;
  • connmanager_test.go媒役、dynamicbanscore_test.go: 定義了相應(yīng)的Test方法;

通過代理或者洋蔥代理進(jìn)行TCP連接的代碼位于btcsuite/go-socks(btcd項(xiàng)目的btcsuite/btcd/vendor/github.com/btcsuite/go-socks目錄)祝谚,它實(shí)現(xiàn)了SOCKS 5協(xié)議的client部分,包含的文件有:

  • addr.go: 定義了ProxiedAddr酣衷,用于描述代理的外部地址交惯,包括網(wǎng)絡(luò)類型(如tcp),主機(jī)名或地址及端口號(hào);
  • conn.go: 定義了proxiedConn,用于描述被代理的連接席爽,提供了讀意荤、寫代理連接的方法等;
  • dial.go: 實(shí)現(xiàn)了建立代理連接的邏輯;

雖然ConnMgr支持通過洋蔥代理與“明網(wǎng)”或者“暗網(wǎng)”中的節(jié)點(diǎn)連接,但本文暫不深入介紹Tor網(wǎng)絡(luò)相關(guān)的知識(shí)只锻,我們將在后文《Bitcoin網(wǎng)絡(luò)與Tor網(wǎng)絡(luò)的匿名性討論》中詳細(xì)介紹玖像。接下來,我們先分析btcd/connmgr來了解連接建立及管理的機(jī)制齐饮,然后分析btcsuite/go-socks來了解通過代理進(jìn)行連接的過程捐寥。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: 記錄主動(dòng)連接其他節(jié)點(diǎn)的連接數(shù)量;
  • start: 標(biāo)識(shí)connmgr已經(jīng)啟動(dòng);
  • stop: 標(biāo)識(shí)connmgr已經(jīng)結(jié)束;
  • cfg: 設(shè)定相關(guān)的配置握恳,在Config的定義中介紹;
  • wg: 用于同步connmgr的退出狀態(tài),調(diào)用方可以阻塞等待connmgr的工作協(xié)程退出;
  • failedAttempts: 某個(gè)連接失敗后捺僻,ConnMgr嘗試選擇新的Peer地址連接的總次數(shù);
  • requests:用于與connmgr工作協(xié)程通信的管道;
  • quit: 用于通知工作協(xié)程退出;

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: 節(jié)點(diǎn)上所有等待外部連接的監(jiān)聽點(diǎn);
  • OnAccept: 節(jié)點(diǎn)應(yīng)答并接受外部連接后的回調(diào)函數(shù);
  • TargetOutbound:節(jié)點(diǎn)主動(dòng)向外連接Peer的最大個(gè)數(shù);
  • RetryDuration: 連接失敗后發(fā)起重連的等待時(shí)間乡洼,默認(rèn)為5s,默認(rèn)的最大重連等待時(shí)間為5min;
  • OnConnection: 連接建立成功后的回調(diào)函數(shù);
  • OnDisconnection: 連接關(guān)閉后的回調(diào)函數(shù);
  • GetNewAddress: 連接失敗后匕坯,ConnMgr可能會(huì)選擇新的Peer進(jìn)行連接束昵,GetNewAddress函數(shù)提供獲取新Peer地址的方法,它最終會(huì)調(diào)用addrManager的GetAddress()來分配新地址葛峻,我們將在介紹addrmgr時(shí)詳細(xì)介紹;
  • Dial: 定義建立TCP連接的方式锹雏,是直連還是通過代理連接;

ConnReq描述了一個(gè)連接,它的定義如下:

//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: 連接的序號(hào)泞歉,用于索引;
  • Addr: 連接的目的地址;
  • Permanent: 標(biāo)識(shí)是否與Peer保持永久連接逼侦,如果為true,則連接失敗后腰耙,繼續(xù)嘗試與該P(yáng)eer連接榛丢,而不是選擇新的Peer地址重新連接;
  • conn: 連接成功后,真實(shí)的net.Conn對(duì)象;
  • state: 連接的狀態(tài)挺庞,有ConnPending晰赞、ConnEstablished、ConnDisconnected及ConnFailed等;
  • stateMtx: 保護(hù)state狀態(tài)的讀寫鎖;
  • retryCount: 如果Permanent為true选侨,retryCount記錄該連接重復(fù)重連的次數(shù);

我們先從ConnManager的Start()方法入手來分析它的工作機(jī)制:

//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啟動(dòng)時(shí)主要有如下過程:

  1. 啟動(dòng)工作協(xié)程connHandler;
  2. 啟動(dòng)監(jiān)聽協(xié)程listenHandler,等待其他節(jié)點(diǎn)連接;
  3. 啟動(dòng)建立連接的協(xié)程援制,選擇Peer地址并主動(dòng)連接;

ConnMgr中各協(xié)程及其通信的channel示意如下圖所示:

其中caller是指調(diào)用協(xié)程戏挡,onConnect、OnDisconnect和OnAccept均在新的協(xié)程中回調(diào)晨仑,以免阻塞ConnMgr的工作協(xié)程和監(jiān)聽協(xié)程褐墅。在開始分析上述三個(gè)協(xié)程之前泡躯,我們先來看看Connect()和Disconnect()方法了解建立和斷開連接的實(shí)現(xiàn):

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

可以看出湘今,建立連接的過程就是調(diào)用指定的Dial()方法來進(jìn)行TCP握手,如果與Peer直連(指不經(jīng)過代理)漠秋,則直接調(diào)用net.Dial()進(jìn)行連接偿短;如果通過代理與Peer連接靶橱,則會(huì)調(diào)用SOCKS Proxy的Dial()方法隘弊,我們將在分析go-socks中看到糕再。然后,根據(jù)是否連接成功向connHandler發(fā)送成功或者失敗的消息艘款,讓connHandler進(jìn)一步處理持际。調(diào)用Disconnect斷開連接則向connHandler發(fā)送handleDisconnected消息讓connHandler進(jìn)一步處理×谆看來选酗,連接或者斷開連接的主要處理邏輯在connHandler中阵难,我們來看看它的實(shí)現(xiàn):

//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主要處理連接建立成功岳枷、失敗和斷連這三種情況:

  1. 如果連接成功,首先更新連接的狀態(tài)為ConnEstablished呜叫,同時(shí)將該連接添加到conns中以跟蹤它的后續(xù)狀態(tài)空繁,并將retryCount和failedAttempts重置,隨后在新的goroutine中回調(diào)OnConnection;
  2. 如果要斷開連接朱庆,先從conns找到要斷開的connReq盛泡,更新連接狀態(tài)為ConnDisconnected,調(diào)用net.Conn的Close()方法斷開TCP連接娱颊,隨后在新的goroutine中回調(diào)OnDisconnection傲诵;最后,如果是當(dāng)前的活躍連接數(shù)少于設(shè)定的最大門限且retry設(shè)為true箱硕,則調(diào)用handleFailedConn進(jìn)行重連或者選擇新的Peer連接;
  3. 如果連接失敗拴竹,則將連接狀態(tài)更新為ConnFailed,同時(shí)調(diào)用handleFailedConn進(jìn)行重連或者選擇新的Peer連接;

需要注意的是剧罩,ConnMgr只處理了連接建立成功或者失敗的情況栓拜,并沒有專門處理連接成功一段時(shí)間后連接中斷的情況,這是因?yàn)門CP socket雖然有keepalive選項(xiàng)開啟心跳惠昔,但并沒有心跳超時(shí)的回調(diào)幕与,只有當(dāng)調(diào)用write()方法寫入數(shù)據(jù)返回錯(cuò)誤時(shí)才能檢測(cè)到連接中斷,所以一般需要應(yīng)用層協(xié)議通過心跳的方式檢測(cè)網(wǎng)絡(luò)中斷的情形镇防。我們?cè)?a href="http://www.reibang.com/p/66dc6f1ea05a" target="_blank">《Btcd區(qū)塊在P2P網(wǎng)絡(luò)上的傳播之Peer》中介紹過啦鸣,Peer之間會(huì)發(fā)送Ping/Pong心跳來維持及檢測(cè)連接。如果Pong消息超時(shí)或者outHandler向net.Conn寫數(shù)據(jù)出錯(cuò)時(shí)来氧,Peer的Disconnect()方法會(huì)被調(diào)用以主動(dòng)斷開連接诫给,并退出Peer的工作協(xié)程饼齿。當(dāng)Peer連接建立成功并回調(diào)OnConnect()時(shí),server會(huì)新起一個(gè)goroutine守護(hù)與Peer的連接狀態(tài)蝙搔;當(dāng)Peer斷連并退出時(shí)缕溉,server隨即會(huì)調(diào)用ConnMgr的Disconnect()方法以清除該連接。

接下來吃型,我們看看handleFailedConn的實(shí)現(xiàn):

//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主要處理重連邏輯证鸥,它的主要思想為:

  1. 如果連接的Permanent為true,即該連接為“持久”連接勤晚,連接失敗進(jìn)需要重連枉层;需要注意的時(shí),重連的等待時(shí)間是與重連的次數(shù)成正比的赐写,即第1次重連需等待5s鸟蜡,第2次重連需要等待10s,以次類推挺邀,最大等待時(shí)間為5min;
  2. 如果連接不是“持久”連接揉忘,則選擇新的Peer進(jìn)行連接,如果嘗試新連接的次數(shù)超限(默認(rèn)為25次)端铛,則表明節(jié)點(diǎn)的出口網(wǎng)絡(luò)可能斷連泣矛,需要延時(shí)連接,默認(rèn)延時(shí)5s;

動(dòng)態(tài)選擇Peer并發(fā)起連接的過程在NewConnReq()中實(shí)現(xiàn):

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

其主要過程為:

  1. 新建ConnReq對(duì)象禾蚕,并為其分配一個(gè)id;
  2. 通過GetNewAddress()從addrmgr維護(hù)的地址倉(cāng)庫(kù)中隨機(jī)選擇一個(gè)Peer的可達(dá)地址您朽,如果地址選擇失敗,則由connHandler再次發(fā)起新的連接;
  3. 調(diào)用Connect()方法開始與Peer建立連接;

上面各方法已經(jīng)展示了ConnMgr主動(dòng)與Peer建立連接换淆,及失敗后重連或者選擇新地址連接的過程哗总,接下來,我們通過listenHandler來看它被動(dòng)等待連接的實(shí)現(xiàn):

//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主要是等待連接讯屈,連接成功后在新協(xié)程中回調(diào)OnAccept。實(shí)際上易猫,OnConnect和OnAccept回調(diào)將在server中實(shí)現(xiàn)耻煤,而是創(chuàng)建Peer并調(diào)用Peer的AssociateConnection()方法的入口,我們將在分析server.go中詳細(xì)介紹准颓。

以上就是ConnMgr建立及維護(hù)連接的主要過程哈蝇。接下來,我們來分析用于防止DDoS攻擊的動(dòng)態(tài)計(jì)分器是如何實(shí)現(xiàn)的攘已,先看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: 上一次調(diào)整分值的Unix時(shí)間點(diǎn);
  • transient: 分值的浮動(dòng)衰減部分;
  • persistent: 分值中不會(huì)自動(dòng)衰減的部分;
  • mtx: 保護(hù)transient和persistent的互斥鎖;

從上面的定義看炮赦,DynamicBanScore提供的分值是由一個(gè)不變值和瞬時(shí)值構(gòu)成的,那么這兩值到底是如何起作用的呢样勃,我們可以看看它的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乘以一個(gè)衰減系數(shù)后的和性芬。其中衰減系數(shù)隨時(shí)間變化,它由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)
}

可以看出剧防,衰減系數(shù)是按時(shí)間間隔呈指數(shù)分布的植锉,其中Lambda=ln2/60。動(dòng)態(tài)分值隨時(shí)間時(shí)隔變化的曲線如下圖所示:


這里的時(shí)間間隔是指當(dāng)前取值時(shí)刻距上一次主動(dòng)調(diào)節(jié)persistent或者transistent值的時(shí)間差峭拘。

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

可以看出俊庇,主動(dòng)調(diào)節(jié)score值時(shí),先將persistent值直接相加鸡挠,然后算出傳入時(shí)刻t的transient值辉饱,再與傳入的transient值相加后得到新的transient值,新的persistent與新的transient值相加后得到新的score拣展。實(shí)際上彭沼,就是t時(shí)刻的score加上傳入的persistent和transient即得到新的score。

Peer之間交換消息時(shí)备埃,每一個(gè)Peer連接會(huì)有一個(gè)動(dòng)態(tài)計(jì)分器來監(jiān)控它們之間收發(fā)消息的頻率姓惑,太頻繁地收到某個(gè)Peer發(fā)過來的消息時(shí),將被懷疑遭到DDoS攻擊瓜喇,從而主動(dòng)斷開與它的連接挺益,我們將在分析協(xié)議消息的收發(fā)時(shí)看到這一點(diǎn)。

通過前面的分析乘寒,我們知道ConnMgr會(huì)通過GetNewAddress()來選取Peer的地址,但一個(gè)新的節(jié)點(diǎn)接入時(shí)匪补,它還沒有與任何Peer交換過地址信息伞辛,所以它的地址倉(cāng)庫(kù)是空的,那它該與哪些節(jié)點(diǎn)先建立連接呢夯缺?實(shí)際上蚤氏,節(jié)點(diǎn)會(huì)內(nèi)置一些種子節(jié)點(diǎn)的地址:

//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節(jié)點(diǎn)內(nèi)置了如上6個(gè)種子節(jié)點(diǎn)的域名。然而踊兜,在ConnMgr連接種子節(jié)點(diǎn)之前竿滨,必須進(jìn)行DNS Lookup查詢它們對(duì)應(yīng)的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)
    }
}

它的主要步驟為:

  1. 調(diào)用lookupFn()進(jìn)行DNS resolve捏境,將種子節(jié)點(diǎn)的域名解析了IP地址;
  2. 將種子節(jié)點(diǎn)的IP地址封裝為協(xié)議地址wire.NetAddress于游,其中主要是增加了地址的時(shí)效性,這里將地址的時(shí)效隨機(jī)地設(shè)為3到7天垫言。

這里傳入的lookupFn()根據(jù)配置贰剥,有可能是節(jié)點(diǎn)自己訪問DNS Server解析,也有可能通過洋蔥代理進(jìn)行解析:

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

    ......
}

從上述代碼可以看出:

  1. 默認(rèn)的DNS Lookup和Dial方法就是標(biāo)準(zhǔn)的net.LookupIP和net.DialTimeout;
  2. 如果設(shè)置了代理筷频,Dial方法將使用SOCKS Proxy的DialTimeout()蚌成,如果未禁用洋蔥代理前痘,則默認(rèn)代理為洋蔥代理,DNS查詢將通過connmgr的TorLookupIP()實(shí)現(xiàn);
  3. 如果專門設(shè)置了洋蔥代理担忧,則設(shè)定對(duì)“暗網(wǎng)”服務(wù)(hidden service)的連接采用SOCKS Proxy的DialTimeout()芹缔,DNS Lookup將使用connmgr的TorLookupIP();請(qǐng)注意瓶盛,即使設(shè)置了洋蔥代理乖菱,對(duì)“明網(wǎng)”地址的連接仍是根據(jù)是否設(shè)置了普通SOCKS代理(非Tor代理)來決定采用標(biāo)準(zhǔn)的net.DialTimeout還是Proxy的DialTimeout;

無論是通過普通代理還是洋蔥代理連接Peer,對(duì)節(jié)點(diǎn)來講蓬网,它們均是SOCKS代理服務(wù)器窒所,節(jié)點(diǎn)與它們之間通過SOCKS協(xié)議來通信。與普通代理相比帆锋,洋蔥代理擴(kuò)展了SOCKS協(xié)議吵取,加入了對(duì)Name lookup、Stream Isolation等的支持锯厢。SOCKS協(xié)議位于會(huì)話層皮官,在傳輸層與應(yīng)用層之間,所以它不僅可以代理HTTP流量实辑,也可以代理如FTP捺氢、XMPP等等的其他應(yīng)用流量。SOCKS協(xié)議比較簡(jiǎn)單剪撬,我們不再展開介紹摄乒,讀者可以閱讀RFC1928RFC1929來了解它的消息格式。為了了解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節(jié)點(diǎn)之間均通過TCP連接馍佑,因此這里實(shí)現(xiàn)的是SOCKS代理TCP連接的情形。建立代理連接的主要步驟為:

  1. 與SOCKS代理服務(wù)器建立TCP連接梨水,如代碼(1)處所示;
  2. 客戶端向代理服務(wù)器發(fā)送協(xié)議版本和METHOD集合的協(xié)商請(qǐng)求拭荤,如代碼(3)處所示,客戶端選擇版本5疫诽,選擇的認(rèn)證方法為不驗(yàn)證或者用戶名/密碼驗(yàn)證舅世,或者僅僅是不認(rèn)證;
  3. 然后等待SOCKS服務(wù)器響應(yīng)。如果SOCKS服務(wù)器不支持SOCKS 5奇徒,則協(xié)商失敵恰;如果SOCKS服務(wù)器支持SOCKS 5逼龟,并同意不驗(yàn)證评凝,則客戶端可以直接發(fā)送后續(xù)請(qǐng)求,如果SOCKS服務(wù)器指定采用用戶名/密碼認(rèn)證腺律,則客戶端隨后向服務(wù)器提交用戶名和密碼奕短,服務(wù)器將驗(yàn)證并返回結(jié)果宜肉,如代碼(4)所示;
  4. 無需要認(rèn)證或者用戶名/密碼驗(yàn)證通過后,客戶端向SOCKS服務(wù)器發(fā)送CONNECT請(qǐng)求翎碑,并指明目的IP和端口號(hào)谬返,如代碼(5)處所示;
  5. SOCKS服務(wù)器響應(yīng)CONNECT請(qǐng)求,如果代理連接成功日杈,則返回外部的代理地址和端口遣铝。根據(jù)響應(yīng)消息中指明的代理地址類型,代理地址可能是IPv4莉擒、IPv6或者Domain Name酿炸。
  6. 創(chuàng)建并返回一個(gè)代理連接對(duì)象proxiedConn,它的conn字段描述客戶端與SOCKS服務(wù)器的TCP連接涨冀,該連接上的TCP報(bào)文將通過代理服務(wù)器轉(zhuǎn)發(fā)給目的地址填硕,boundAddr描述代理的外部地址和端口,remoteAddr描述目的地址與端口鹿鳖。

特別地扁眯,如果客戶端連接一個(gè)Tor代理,并且希望開啟Stream Isolation特性翅帜,則隨機(jī)生成用戶名和密碼并發(fā)往Tor代理服務(wù)器姻檀。Stream Isolation是為了禁止Tor網(wǎng)絡(luò)在同一個(gè)“虛電路”上中繼不同的TCP流,Tor代理服務(wù)器支持通過IsolateClientAddr涝滴、IsolateSOCKSAuth绣版、IsolateClientProtocol、IsolateDestPort及IsolateDestAddr等方式來標(biāo)識(shí)不同的TCP流狭莱。Btcd選擇通過IsolateSOCKSAuth來支持Stream Isolation僵娃,使得同一節(jié)點(diǎn)在連接不同Peer或者重連相同Peer時(shí)的TCP在Tor網(wǎng)絡(luò)中均能被“隔離”。然而腋妙,讀者可能會(huì)產(chǎn)生疑問: 隨機(jī)生成的用戶名和密碼如何被Tor代理服務(wù)器驗(yàn)證?實(shí)際上讯榕,Btcd這里使用隨機(jī)用戶名和密碼骤素,是要求Tor代理服務(wù)器作如下配置: 選擇“NO AUTHENTICATION REQUIRED”作為驗(yàn)證方式,并且只通過username來標(biāo)識(shí)不同代理請(qǐng)求愚屁。

了解了通過SOCKS代理或者Tor代理與Peer建立TCP連接的機(jī)制后济竹,我們就可以來看看如何通過Tor代理來進(jìn)行DNS查詢。再次強(qiáng)調(diào)一下霎槐,通過Tor代理進(jìn)行DNS查詢不是解析洋蔥地址送浊,而是解析“明網(wǎng)”中的域名。例如丘跌,用戶通過Tor代理訪問www.google.com時(shí)袭景,用戶可以選擇先通過DNS查詢到IP地址后唁桩,再通過Tor代理連接該IP地址;也可以將該域名作為目的地址發(fā)給Tor代理耸棒,讓Tor網(wǎng)絡(luò)的退出結(jié)點(diǎn)進(jìn)行DNS查詢荒澡,并建立與目的地址的連接。如果某些客戶端不希望向DNS Server暴露自己的目標(biāo)訪問域名与殃,同時(shí)又希望進(jìn)行域名解析单山,那它可以通過Tor代理進(jìn)行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
}

其過程與建立代理連接的方程類似幅疼,即先協(xié)商版本與認(rèn)證方式米奸,再發(fā)送請(qǐng)求與等待響應(yīng)。不同的地方在于:

  1. 選擇不認(rèn)證的方式爽篷,如代碼(1)處所示;
  2. 請(qǐng)求的命令是'FO'悴晰,它是Tor代理擴(kuò)展的命令,指明用于Name Lookup狼忱,同時(shí)目標(biāo)地址類型指定為DOMAINNAME膨疏,如代碼(2)處所示;
  3. Tor退出節(jié)點(diǎn)進(jìn)行DNS查詢后,由Tor代碼返回钻弄。這里僅接受IPv4地址佃却,如代碼(3)處所示;

到此,我們就完整分析了Bitcoin P2P網(wǎng)絡(luò)中Peer節(jié)點(diǎn)之間建立窘俺、維持和斷開TCP連接的所有過程饲帅,包括了通過SOCKS代理或Tor代理進(jìn)行連接或DNS查詢的實(shí)現(xiàn)。然而瘤泪,我們也了解到灶泵,除了節(jié)點(diǎn)內(nèi)置的種子節(jié)點(diǎn)的地址,節(jié)點(diǎn)接入網(wǎng)絡(luò)時(shí)并不知道其他節(jié)點(diǎn)的地址对途,那么節(jié)點(diǎn)是如何知道網(wǎng)絡(luò)中其他節(jié)點(diǎn)的地址赦邻,以及如何選擇Peer節(jié)點(diǎn)地址建立連接呢?我們將在《Btcd區(qū)塊在P2P網(wǎng)絡(luò)上的傳播之AddrManager》中分析实檀。由于本文涉及到了Tor網(wǎng)絡(luò)惶洲,有些讀者可能希望進(jìn)一步了解Tor,同時(shí)膳犹,Bitcoin網(wǎng)絡(luò)與Tor網(wǎng)絡(luò)均做到了對(duì)源或者賬戶匿名恬吕,所以我們?cè)诜治鯝ddrManager之前,下一篇文章將討論Bitcoin網(wǎng)絡(luò)與Tor網(wǎng)絡(luò)匿名性须床。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末铐料,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌钠惩,老刑警劉巖柒凉,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異妻柒,居然都是意外死亡扛拨,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門举塔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绑警,“玉大人,你說我怎么就攤上這事央渣〖坪校” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵芽丹,是天一觀的道長(zhǎng)北启。 經(jīng)常有香客問我,道長(zhǎng)拔第,這世上最難降的妖魔是什么咕村? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮蚊俺,結(jié)果婚禮上懈涛,老公的妹妹穿的比我還像新娘。我一直安慰自己泳猬,他們只是感情好批钠,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著得封,像睡著了一般埋心。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上忙上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天拷呆,我揣著相機(jī)與錄音,去河邊找鬼疫粥。 笑死洋腮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的手形。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼悯恍,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼库糠!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤瞬欧,失蹤者是張志新(化名)和其女友劉穎贷屎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體艘虎,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡唉侄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了野建。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片属划。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖候生,靈堂內(nèi)的尸體忽然破棺而出同眯,到底是詐尸還是另有隱情,我是刑警寧澤唯鸭,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布须蜗,位于F島的核電站,受9級(jí)特大地震影響目溉,放射性物質(zhì)發(fā)生泄漏明肮。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一缭付、第九天 我趴在偏房一處隱蔽的房頂上張望柿估。 院中可真熱鬧,春花似錦蛉腌、人聲如沸官份。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽舅巷。三九已至,卻和暖如春河咽,著一層夾襖步出監(jiān)牢的瞬間钠右,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工忘蟹, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留飒房,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓媚值,卻偏偏與公主長(zhǎng)得像狠毯,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子褥芒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容