Integrate BACKBEAT SDK and resolve KACHING license validation

Major integrations and fixes:
- Added BACKBEAT SDK integration for P2P operation timing
- Implemented beat-aware status tracking for distributed operations
- Added Docker secrets support for secure license management
- Resolved KACHING license validation via HTTPS/TLS
- Updated docker-compose configuration for clean stack deployment
- Disabled rollback policies to prevent deployment failures
- Added license credential storage (CHORUS-DEV-MULTI-001)

Technical improvements:
- BACKBEAT P2P operation tracking with phase management
- Enhanced configuration system with file-based secrets
- Improved error handling for license validation
- Clean separation of KACHING and CHORUS deployment stacks

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-09-06 07:56:26 +10:00
parent 543ab216f9
commit 9bdcbe0447
4730 changed files with 1480093 additions and 1916 deletions

View File

@@ -0,0 +1,76 @@
package client
import (
"context"
"io"
"sync"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/transport"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
logging "github.com/ipfs/go-log/v2"
)
var log = logging.Logger("p2p-circuit")
// Client implements the client-side of the p2p-circuit/v2 protocol:
// - it implements dialing through v2 relays
// - it listens for incoming connections through v2 relays.
//
// For backwards compatibility with v1 relays and older nodes, the client will
// also accept relay connections through v1 relays and fallback dial peers using p2p-circuit/v1.
// This allows us to use the v2 code as drop in replacement for v1 in a host without breaking
// existing code and interoperability with older nodes.
type Client struct {
ctx context.Context
ctxCancel context.CancelFunc
host host.Host
upgrader transport.Upgrader
incoming chan accept
mx sync.Mutex
activeDials map[peer.ID]*completion
hopCount map[peer.ID]int
}
var _ io.Closer = &Client{}
var _ transport.Transport = &Client{}
type accept struct {
conn *Conn
writeResponse func() error
}
type completion struct {
ch chan struct{}
relay peer.ID
err error
}
// New constructs a new p2p-circuit/v2 client, attached to the given host and using the given
// upgrader to perform connection upgrades.
func New(h host.Host, upgrader transport.Upgrader) (*Client, error) {
cl := &Client{
host: h,
upgrader: upgrader,
incoming: make(chan accept),
activeDials: make(map[peer.ID]*completion),
hopCount: make(map[peer.ID]int),
}
cl.ctx, cl.ctxCancel = context.WithCancel(context.Background())
return cl, nil
}
// Start registers the circuit (client) protocol stream handlers
func (c *Client) Start() {
c.host.SetStreamHandler(proto.ProtoIDv2Stop, c.handleStreamV2)
}
func (c *Client) Close() error {
c.ctxCancel()
c.host.RemoveStreamHandler(proto.ProtoIDv2Stop)
return nil
}

View File

@@ -0,0 +1,163 @@
package client
import (
"fmt"
"net"
"time"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
tpt "github.com/libp2p/go-libp2p/core/transport"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
// HopTagWeight is the connection manager weight for connections carrying relay hop streams
var HopTagWeight = 5
type statLimitDuration struct{}
type statLimitData struct{}
var (
StatLimitDuration = statLimitDuration{}
StatLimitData = statLimitData{}
)
type Conn struct {
stream network.Stream
remote peer.AddrInfo
stat network.ConnStats
client *Client
}
type NetAddr struct {
Relay string
Remote string
}
var _ net.Addr = (*NetAddr)(nil)
func (n *NetAddr) Network() string {
return "libp2p-circuit-relay"
}
func (n *NetAddr) String() string {
return fmt.Sprintf("relay[%s-%s]", n.Remote, n.Relay)
}
// Conn interface
var _ manet.Conn = (*Conn)(nil)
func (c *Conn) Close() error {
c.untagHop()
return c.stream.Reset()
}
func (c *Conn) Read(buf []byte) (int, error) {
return c.stream.Read(buf)
}
func (c *Conn) Write(buf []byte) (int, error) {
return c.stream.Write(buf)
}
func (c *Conn) SetDeadline(t time.Time) error {
return c.stream.SetDeadline(t)
}
func (c *Conn) SetReadDeadline(t time.Time) error {
return c.stream.SetReadDeadline(t)
}
func (c *Conn) SetWriteDeadline(t time.Time) error {
return c.stream.SetWriteDeadline(t)
}
// TODO: is it okay to cast c.Conn().RemotePeer() into a multiaddr? might be "user input"
func (c *Conn) RemoteMultiaddr() ma.Multiaddr {
// TODO: We should be able to do this directly without converting to/from a string.
relayAddr, err := ma.NewComponent(
ma.ProtocolWithCode(ma.P_P2P).Name,
c.stream.Conn().RemotePeer().String(),
)
if err != nil {
panic(err)
}
return ma.Join(c.stream.Conn().RemoteMultiaddr(), relayAddr, circuitAddr)
}
func (c *Conn) LocalMultiaddr() ma.Multiaddr {
return c.stream.Conn().LocalMultiaddr()
}
func (c *Conn) LocalAddr() net.Addr {
na, err := manet.ToNetAddr(c.stream.Conn().LocalMultiaddr())
if err != nil {
log.Error("failed to convert local multiaddr to net addr:", err)
return nil
}
return na
}
func (c *Conn) RemoteAddr() net.Addr {
return &NetAddr{
Relay: c.stream.Conn().RemotePeer().String(),
Remote: c.remote.ID.String(),
}
}
// ConnStat interface
var _ network.ConnStat = (*Conn)(nil)
func (c *Conn) Stat() network.ConnStats {
return c.stat
}
// tagHop tags the underlying relay connection so that it can be (somewhat) protected from the
// connection manager as it is an important connection that proxies other connections.
// This is handled here so that the user code doesnt need to bother with this and avoid
// clown shoes situations where a high value peer connection is behind a relayed connection and it is
// implicitly because the connection manager closed the underlying relay connection.
func (c *Conn) tagHop() {
c.client.mx.Lock()
defer c.client.mx.Unlock()
p := c.stream.Conn().RemotePeer()
c.client.hopCount[p]++
if c.client.hopCount[p] == 1 {
c.client.host.ConnManager().TagPeer(p, "relay-hop-stream", HopTagWeight)
}
}
// untagHop removes the relay-hop-stream tag if necessary; it is invoked when a relayed connection
// is closed.
func (c *Conn) untagHop() {
c.client.mx.Lock()
defer c.client.mx.Unlock()
p := c.stream.Conn().RemotePeer()
c.client.hopCount[p]--
if c.client.hopCount[p] == 0 {
c.client.host.ConnManager().UntagPeer(p, "relay-hop-stream")
delete(c.client.hopCount, p)
}
}
type capableConnWithStat interface {
tpt.CapableConn
network.ConnStat
}
type capableConn struct {
capableConnWithStat
}
var transportName = ma.ProtocolWithCode(ma.P_CIRCUIT).Name
func (c capableConn) ConnState() network.ConnectionState {
return network.ConnectionState{
Transport: transportName,
}
}

View File

@@ -0,0 +1,189 @@
package client
import (
"context"
"fmt"
"time"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
pbv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/pb"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/util"
ma "github.com/multiformats/go-multiaddr"
)
const maxMessageSize = 4096
var DialTimeout = time.Minute
var DialRelayTimeout = 5 * time.Second
// relay protocol errors; used for signalling deduplication
type relayError struct {
err string
}
func (e relayError) Error() string {
return e.err
}
func newRelayError(t string, args ...interface{}) error {
return relayError{err: fmt.Sprintf(t, args...)}
}
func isRelayError(err error) bool {
_, ok := err.(relayError)
return ok
}
// dialer
func (c *Client) dial(ctx context.Context, a ma.Multiaddr, p peer.ID) (*Conn, error) {
// split /a/p2p-circuit/b into (/a, /p2p-circuit/b)
relayaddr, destaddr := ma.SplitFunc(a, func(c ma.Component) bool {
return c.Protocol().Code == ma.P_CIRCUIT
})
// If the address contained no /p2p-circuit part, the second part is nil.
if destaddr == nil {
return nil, fmt.Errorf("%s is not a relay address", a)
}
if relayaddr == nil {
return nil, fmt.Errorf("can't dial a p2p-circuit without specifying a relay: %s", a)
}
dinfo := peer.AddrInfo{ID: p}
// Strip the /p2p-circuit prefix from the destaddr so that we can pass the destination address
// (if present) for active relays
_, destaddr = ma.SplitFirst(destaddr)
if destaddr != nil {
dinfo.Addrs = append(dinfo.Addrs, destaddr)
}
rinfo, err := peer.AddrInfoFromP2pAddr(relayaddr)
if err != nil {
return nil, fmt.Errorf("error parsing relay multiaddr '%s': %w", relayaddr, err)
}
// deduplicate active relay dials to the same peer
retry:
c.mx.Lock()
dedup, active := c.activeDials[p]
if !active {
dedup = &completion{ch: make(chan struct{}), relay: rinfo.ID}
c.activeDials[p] = dedup
}
c.mx.Unlock()
if active {
select {
case <-dedup.ch:
if dedup.err != nil {
if dedup.relay != rinfo.ID {
// different relay, retry
goto retry
}
if !isRelayError(dedup.err) {
// not a relay protocol error, retry
goto retry
}
// don't try the same relay if it failed to connect with a protocol error
return nil, fmt.Errorf("concurrent active dial through the same relay failed with a protocol error")
}
return nil, fmt.Errorf("concurrent active dial succeeded")
case <-ctx.Done():
return nil, ctx.Err()
}
}
conn, err := c.dialPeer(ctx, *rinfo, dinfo)
c.mx.Lock()
dedup.err = err
close(dedup.ch)
delete(c.activeDials, p)
c.mx.Unlock()
return conn, err
}
func (c *Client) dialPeer(ctx context.Context, relay, dest peer.AddrInfo) (*Conn, error) {
log.Debugf("dialing peer %s through relay %s", dest.ID, relay.ID)
if len(relay.Addrs) > 0 {
c.host.Peerstore().AddAddrs(relay.ID, relay.Addrs, peerstore.TempAddrTTL)
}
dialCtx, cancel := context.WithTimeout(ctx, DialRelayTimeout)
defer cancel()
s, err := c.host.NewStream(dialCtx, relay.ID, proto.ProtoIDv2Hop)
if err != nil {
return nil, fmt.Errorf("error opening hop stream to relay: %w", err)
}
return c.connect(s, dest)
}
func (c *Client) connect(s network.Stream, dest peer.AddrInfo) (*Conn, error) {
if err := s.Scope().ReserveMemory(maxMessageSize, network.ReservationPriorityAlways); err != nil {
s.Reset()
return nil, err
}
defer s.Scope().ReleaseMemory(maxMessageSize)
rd := util.NewDelimitedReader(s, maxMessageSize)
wr := util.NewDelimitedWriter(s)
defer rd.Close()
var msg pbv2.HopMessage
msg.Type = pbv2.HopMessage_CONNECT.Enum()
msg.Peer = util.PeerInfoToPeerV2(dest)
s.SetDeadline(time.Now().Add(DialTimeout))
err := wr.WriteMsg(&msg)
if err != nil {
s.Reset()
return nil, err
}
msg.Reset()
err = rd.ReadMsg(&msg)
if err != nil {
s.Reset()
return nil, err
}
s.SetDeadline(time.Time{})
if msg.GetType() != pbv2.HopMessage_STATUS {
s.Reset()
return nil, newRelayError("unexpected relay response; not a status message (%d)", msg.GetType())
}
status := msg.GetStatus()
if status != pbv2.Status_OK {
s.Reset()
return nil, newRelayError("error opening relay circuit: %s (%d)", pbv2.Status_name[int32(status)], status)
}
// check for a limit provided by the relay; if the limit is not nil, then this is a limited
// relay connection and we mark the connection as transient.
var stat network.ConnStats
if limit := msg.GetLimit(); limit != nil {
stat.Transient = true
stat.Extra = make(map[interface{}]interface{})
stat.Extra[StatLimitDuration] = time.Duration(limit.GetDuration()) * time.Second
stat.Extra[StatLimitData] = limit.GetData()
}
return &Conn{stream: s, remote: dest, stat: stat, client: c}, nil
}

View File

@@ -0,0 +1,88 @@
package client
import (
"time"
"github.com/libp2p/go-libp2p/core/network"
pbv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/pb"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/util"
)
var (
StreamTimeout = 1 * time.Minute
AcceptTimeout = 10 * time.Second
)
func (c *Client) handleStreamV2(s network.Stream) {
log.Debugf("new relay/v2 stream from: %s", s.Conn().RemotePeer())
s.SetReadDeadline(time.Now().Add(StreamTimeout))
rd := util.NewDelimitedReader(s, maxMessageSize)
defer rd.Close()
writeResponse := func(status pbv2.Status) error {
wr := util.NewDelimitedWriter(s)
var msg pbv2.StopMessage
msg.Type = pbv2.StopMessage_STATUS.Enum()
msg.Status = status.Enum()
return wr.WriteMsg(&msg)
}
handleError := func(status pbv2.Status) {
log.Debugf("protocol error: %s (%d)", pbv2.Status_name[int32(status)], status)
err := writeResponse(status)
if err != nil {
s.Reset()
log.Debugf("error writing circuit response: %s", err.Error())
} else {
s.Close()
}
}
var msg pbv2.StopMessage
err := rd.ReadMsg(&msg)
if err != nil {
handleError(pbv2.Status_MALFORMED_MESSAGE)
return
}
// reset stream deadline as message has been read
s.SetReadDeadline(time.Time{})
if msg.GetType() != pbv2.StopMessage_CONNECT {
handleError(pbv2.Status_UNEXPECTED_MESSAGE)
return
}
src, err := util.PeerToPeerInfoV2(msg.GetPeer())
if err != nil {
handleError(pbv2.Status_MALFORMED_MESSAGE)
return
}
// check for a limit provided by the relay; if the limit is not nil, then this is a limited
// relay connection and we mark the connection as transient.
var stat network.ConnStats
if limit := msg.GetLimit(); limit != nil {
stat.Transient = true
stat.Extra = make(map[interface{}]interface{})
stat.Extra[StatLimitDuration] = time.Duration(limit.GetDuration()) * time.Second
stat.Extra[StatLimitData] = limit.GetData()
}
log.Debugf("incoming relay connection from: %s", src.ID)
select {
case c.incoming <- accept{
conn: &Conn{stream: s, remote: src, stat: stat, client: c},
writeResponse: func() error {
return writeResponse(pbv2.Status_OK)
},
}:
case <-time.After(AcceptTimeout):
handleError(pbv2.Status_CONNECTION_FAILED)
}
}

View File

@@ -0,0 +1,54 @@
package client
import (
"net"
"github.com/libp2p/go-libp2p/core/transport"
ma "github.com/multiformats/go-multiaddr"
manet "github.com/multiformats/go-multiaddr/net"
)
var _ manet.Listener = (*Listener)(nil)
type Listener Client
func (c *Client) Listener() *Listener {
return (*Listener)(c)
}
func (l *Listener) Accept() (manet.Conn, error) {
for {
select {
case evt := <-l.incoming:
err := evt.writeResponse()
if err != nil {
log.Debugf("error writing relay response: %s", err.Error())
evt.conn.stream.Reset()
continue
}
log.Debugf("accepted relay connection from %s through %s", evt.conn.remote.ID, evt.conn.RemoteMultiaddr())
evt.conn.tagHop()
return evt.conn, nil
case <-l.ctx.Done():
return nil, transport.ErrListenerClosed
}
}
}
func (l *Listener) Addr() net.Addr {
return &NetAddr{
Relay: "any",
Remote: "any",
}
}
func (l *Listener) Multiaddr() ma.Multiaddr {
return circuitAddr
}
func (l *Listener) Close() error {
return (*Client)(l).Close()
}

View File

@@ -0,0 +1,159 @@
package client
import (
"context"
"fmt"
"time"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/peerstore"
"github.com/libp2p/go-libp2p/core/record"
pbv2 "github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/pb"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/proto"
"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/util"
ma "github.com/multiformats/go-multiaddr"
)
var ReserveTimeout = time.Minute
// Reservation is a struct carrying information about a relay/v2 slot reservation.
type Reservation struct {
// Expiration is the expiration time of the reservation
Expiration time.Time
// Addrs contains the vouched public addresses of the reserving peer, which can be
// announced to the network
Addrs []ma.Multiaddr
// LimitDuration is the time limit for which the relay will keep a relayed connection
// open. If 0, there is no limit.
LimitDuration time.Duration
// LimitData is the number of bytes that the relay will relay in each direction before
// resetting a relayed connection.
LimitData uint64
// Voucher is a signed reservation voucher provided by the relay
Voucher *proto.ReservationVoucher
}
// ReservationError is the error returned on failure to reserve a slot in the relay
type ReservationError struct {
// Status is the status returned by the relay for rejecting the reservation
// request. It is set to pbv2.Status_CONNECTION_FAILED on other failures
Status pbv2.Status
// Reason is the reason for reservation failure
Reason string
err error
}
func (re ReservationError) Error() string {
return fmt.Sprintf("reservation error: status: %s reason: %s err: %s", pbv2.Status_name[int32(re.Status)], re.Reason, re.err)
}
func (re ReservationError) Unwrap() error {
return re.err
}
// Reserve reserves a slot in a relay and returns the reservation information.
// Clients must reserve slots in order for the relay to relay connections to them.
func Reserve(ctx context.Context, h host.Host, ai peer.AddrInfo) (*Reservation, error) {
if len(ai.Addrs) > 0 {
h.Peerstore().AddAddrs(ai.ID, ai.Addrs, peerstore.TempAddrTTL)
}
s, err := h.NewStream(ctx, ai.ID, proto.ProtoIDv2Hop)
if err != nil {
return nil, ReservationError{Status: pbv2.Status_CONNECTION_FAILED, Reason: "failed to open stream", err: err}
}
defer s.Close()
rd := util.NewDelimitedReader(s, maxMessageSize)
wr := util.NewDelimitedWriter(s)
defer rd.Close()
var msg pbv2.HopMessage
msg.Type = pbv2.HopMessage_RESERVE.Enum()
s.SetDeadline(time.Now().Add(ReserveTimeout))
if err := wr.WriteMsg(&msg); err != nil {
s.Reset()
return nil, ReservationError{Status: pbv2.Status_CONNECTION_FAILED, Reason: "error writing reservation message", err: err}
}
msg.Reset()
if err := rd.ReadMsg(&msg); err != nil {
s.Reset()
return nil, ReservationError{Status: pbv2.Status_CONNECTION_FAILED, Reason: "error reading reservation response message: %w", err: err}
}
if msg.GetType() != pbv2.HopMessage_STATUS {
return nil, ReservationError{
Status: pbv2.Status_MALFORMED_MESSAGE,
Reason: fmt.Sprintf("unexpected relay response: not a status message (%d)", msg.GetType()),
err: err}
}
if status := msg.GetStatus(); status != pbv2.Status_OK {
return nil, ReservationError{Status: msg.GetStatus(), Reason: "reservation failed"}
}
rsvp := msg.GetReservation()
if rsvp == nil {
return nil, ReservationError{Status: pbv2.Status_MALFORMED_MESSAGE, Reason: "missing reservation info"}
}
result := &Reservation{}
result.Expiration = time.Unix(int64(rsvp.GetExpire()), 0)
if result.Expiration.Before(time.Now()) {
return nil, ReservationError{
Status: pbv2.Status_MALFORMED_MESSAGE,
Reason: fmt.Sprintf("received reservation with expiration date in the past: %s", result.Expiration),
}
}
addrs := rsvp.GetAddrs()
result.Addrs = make([]ma.Multiaddr, 0, len(addrs))
for _, ab := range addrs {
a, err := ma.NewMultiaddrBytes(ab)
if err != nil {
log.Warnf("ignoring unparsable relay address: %s", err)
continue
}
result.Addrs = append(result.Addrs, a)
}
voucherBytes := rsvp.GetVoucher()
if voucherBytes != nil {
_, rec, err := record.ConsumeEnvelope(voucherBytes, proto.RecordDomain)
if err != nil {
return nil, ReservationError{
Status: pbv2.Status_MALFORMED_MESSAGE,
Reason: fmt.Sprintf("error consuming voucher envelope: %s", err),
err: err,
}
}
voucher, ok := rec.(*proto.ReservationVoucher)
if !ok {
return nil, ReservationError{
Status: pbv2.Status_MALFORMED_MESSAGE,
Reason: fmt.Sprintf("unexpected voucher record type: %+T", rec),
}
}
result.Voucher = voucher
}
limit := msg.GetLimit()
if limit != nil {
result.LimitDuration = time.Duration(limit.GetDuration()) * time.Second
result.LimitData = limit.GetData()
}
return result, nil
}

View File

@@ -0,0 +1,100 @@
package client
import (
"context"
"fmt"
"io"
"github.com/libp2p/go-libp2p/core/host"
"github.com/libp2p/go-libp2p/core/network"
"github.com/libp2p/go-libp2p/core/peer"
"github.com/libp2p/go-libp2p/core/transport"
ma "github.com/multiformats/go-multiaddr"
)
var circuitProtocol = ma.ProtocolWithCode(ma.P_CIRCUIT)
var circuitAddr = ma.Cast(circuitProtocol.VCode)
// AddTransport constructs a new p2p-circuit/v2 client and adds it as a transport to the
// host network
func AddTransport(h host.Host, upgrader transport.Upgrader) error {
n, ok := h.Network().(transport.TransportNetwork)
if !ok {
return fmt.Errorf("%v is not a transport network", h.Network())
}
c, err := New(h, upgrader)
if err != nil {
return fmt.Errorf("error constructing circuit client: %w", err)
}
err = n.AddTransport(c)
if err != nil {
return fmt.Errorf("error adding circuit transport: %w", err)
}
err = n.Listen(circuitAddr)
if err != nil {
return fmt.Errorf("error listening to circuit addr: %w", err)
}
c.Start()
return nil
}
// Transport interface
var _ transport.Transport = (*Client)(nil)
var _ io.Closer = (*Client)(nil)
func (c *Client) Dial(ctx context.Context, a ma.Multiaddr, p peer.ID) (transport.CapableConn, error) {
connScope, err := c.host.Network().ResourceManager().OpenConnection(network.DirOutbound, false, a)
if err != nil {
return nil, err
}
conn, err := c.dialAndUpgrade(ctx, a, p, connScope)
if err != nil {
connScope.Done()
return nil, err
}
return conn, nil
}
func (c *Client) dialAndUpgrade(ctx context.Context, a ma.Multiaddr, p peer.ID, connScope network.ConnManagementScope) (transport.CapableConn, error) {
if err := connScope.SetPeer(p); err != nil {
return nil, err
}
conn, err := c.dial(ctx, a, p)
if err != nil {
return nil, err
}
conn.tagHop()
cc, err := c.upgrader.Upgrade(ctx, c, conn, network.DirOutbound, p, connScope)
if err != nil {
return nil, err
}
return capableConn{cc.(capableConnWithStat)}, nil
}
func (c *Client) CanDial(addr ma.Multiaddr) bool {
_, err := addr.ValueForProtocol(ma.P_CIRCUIT)
return err == nil
}
func (c *Client) Listen(addr ma.Multiaddr) (transport.Listener, error) {
// TODO connect to the relay and reserve slot if specified
if _, err := addr.ValueForProtocol(ma.P_CIRCUIT); err != nil {
return nil, err
}
return c.upgrader.UpgradeListener(c, c.Listener()), nil
}
func (c *Client) Protocols() []int {
return []int{ma.P_CIRCUIT}
}
func (c *Client) Proxy() bool {
return true
}