mirror of
https://github.com/XTLS/Xray-core.git
synced 2024-11-09 18:43:12 +00:00
554 lines
18 KiB
Go
554 lines
18 KiB
Go
// Package proxy contains all proxies used by Xray.
|
|
//
|
|
// To implement an inbound or outbound proxy, one needs to do the following:
|
|
// 1. Implement the interface(s) below.
|
|
// 2. Register a config creator through common.RegisterConfig.
|
|
package proxy
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/rand"
|
|
"io"
|
|
"math/big"
|
|
"runtime"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/pires/go-proxyproto"
|
|
"github.com/xtls/xray-core/app/dispatcher"
|
|
"github.com/xtls/xray-core/common/buf"
|
|
"github.com/xtls/xray-core/common/errors"
|
|
"github.com/xtls/xray-core/common/net"
|
|
"github.com/xtls/xray-core/common/protocol"
|
|
"github.com/xtls/xray-core/common/session"
|
|
"github.com/xtls/xray-core/common/signal"
|
|
"github.com/xtls/xray-core/features/routing"
|
|
"github.com/xtls/xray-core/features/stats"
|
|
"github.com/xtls/xray-core/transport"
|
|
"github.com/xtls/xray-core/transport/internet"
|
|
"github.com/xtls/xray-core/transport/internet/reality"
|
|
"github.com/xtls/xray-core/transport/internet/stat"
|
|
"github.com/xtls/xray-core/transport/internet/tls"
|
|
)
|
|
|
|
var (
|
|
Tls13SupportedVersions = []byte{0x00, 0x2b, 0x00, 0x02, 0x03, 0x04}
|
|
TlsClientHandShakeStart = []byte{0x16, 0x03}
|
|
TlsServerHandShakeStart = []byte{0x16, 0x03, 0x03}
|
|
TlsApplicationDataStart = []byte{0x17, 0x03, 0x03}
|
|
|
|
Tls13CipherSuiteDic = map[uint16]string{
|
|
0x1301: "TLS_AES_128_GCM_SHA256",
|
|
0x1302: "TLS_AES_256_GCM_SHA384",
|
|
0x1303: "TLS_CHACHA20_POLY1305_SHA256",
|
|
0x1304: "TLS_AES_128_CCM_SHA256",
|
|
0x1305: "TLS_AES_128_CCM_8_SHA256",
|
|
}
|
|
)
|
|
|
|
const (
|
|
TlsHandshakeTypeClientHello byte = 0x01
|
|
TlsHandshakeTypeServerHello byte = 0x02
|
|
|
|
CommandPaddingContinue byte = 0x00
|
|
CommandPaddingEnd byte = 0x01
|
|
CommandPaddingDirect byte = 0x02
|
|
)
|
|
|
|
// An Inbound processes inbound connections.
|
|
type Inbound interface {
|
|
// Network returns a list of networks that this inbound supports. Connections with not-supported networks will not be passed into Process().
|
|
Network() []net.Network
|
|
|
|
// Process processes a connection of given network. If necessary, the Inbound can dispatch the connection to an Outbound.
|
|
Process(context.Context, net.Network, stat.Connection, routing.Dispatcher) error
|
|
}
|
|
|
|
// An Outbound process outbound connections.
|
|
type Outbound interface {
|
|
// Process processes the given connection. The given dialer may be used to dial a system outbound connection.
|
|
Process(context.Context, *transport.Link, internet.Dialer) error
|
|
}
|
|
|
|
// UserManager is the interface for Inbounds and Outbounds that can manage their users.
|
|
type UserManager interface {
|
|
// AddUser adds a new user.
|
|
AddUser(context.Context, *protocol.MemoryUser) error
|
|
|
|
// RemoveUser removes a user by email.
|
|
RemoveUser(context.Context, string) error
|
|
}
|
|
|
|
type GetInbound interface {
|
|
GetInbound() Inbound
|
|
}
|
|
|
|
type GetOutbound interface {
|
|
GetOutbound() Outbound
|
|
}
|
|
|
|
// TrafficState is used to track uplink and downlink of one connection
|
|
// It is used by XTLS to determine if switch to raw copy mode, It is used by Vision to calculate padding
|
|
type TrafficState struct {
|
|
UserUUID []byte
|
|
NumberOfPacketToFilter int
|
|
EnableXtls bool
|
|
IsTLS12orAbove bool
|
|
IsTLS bool
|
|
Cipher uint16
|
|
RemainingServerHello int32
|
|
|
|
// reader link state
|
|
WithinPaddingBuffers bool
|
|
ReaderSwitchToDirectCopy bool
|
|
RemainingCommand int32
|
|
RemainingContent int32
|
|
RemainingPadding int32
|
|
CurrentCommand int
|
|
|
|
// write link state
|
|
IsPadding bool
|
|
WriterSwitchToDirectCopy bool
|
|
}
|
|
|
|
func NewTrafficState(userUUID []byte) *TrafficState {
|
|
return &TrafficState{
|
|
UserUUID: userUUID,
|
|
NumberOfPacketToFilter: 8,
|
|
EnableXtls: false,
|
|
IsTLS12orAbove: false,
|
|
IsTLS: false,
|
|
Cipher: 0,
|
|
RemainingServerHello: -1,
|
|
WithinPaddingBuffers: true,
|
|
ReaderSwitchToDirectCopy: false,
|
|
RemainingCommand: -1,
|
|
RemainingContent: -1,
|
|
RemainingPadding: -1,
|
|
CurrentCommand: 0,
|
|
IsPadding: true,
|
|
WriterSwitchToDirectCopy: false,
|
|
}
|
|
}
|
|
|
|
// VisionReader is used to read xtls vision protocol
|
|
// Note Vision probably only make sense as the inner most layer of reader, since it need assess traffic state from origin proxy traffic
|
|
type VisionReader struct {
|
|
buf.Reader
|
|
trafficState *TrafficState
|
|
ctx context.Context
|
|
}
|
|
|
|
func NewVisionReader(reader buf.Reader, state *TrafficState, context context.Context) *VisionReader {
|
|
return &VisionReader{
|
|
Reader: reader,
|
|
trafficState: state,
|
|
ctx: context,
|
|
}
|
|
}
|
|
|
|
func (w *VisionReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
|
buffer, err := w.Reader.ReadMultiBuffer()
|
|
if !buffer.IsEmpty() {
|
|
if w.trafficState.WithinPaddingBuffers || w.trafficState.NumberOfPacketToFilter > 0 {
|
|
mb2 := make(buf.MultiBuffer, 0, len(buffer))
|
|
for _, b := range buffer {
|
|
newbuffer := XtlsUnpadding(b, w.trafficState, w.ctx)
|
|
if newbuffer.Len() > 0 {
|
|
mb2 = append(mb2, newbuffer)
|
|
}
|
|
}
|
|
buffer = mb2
|
|
if w.trafficState.RemainingContent > 0 || w.trafficState.RemainingPadding > 0 || w.trafficState.CurrentCommand == 0 {
|
|
w.trafficState.WithinPaddingBuffers = true
|
|
} else if w.trafficState.CurrentCommand == 1 {
|
|
w.trafficState.WithinPaddingBuffers = false
|
|
} else if w.trafficState.CurrentCommand == 2 {
|
|
w.trafficState.WithinPaddingBuffers = false
|
|
w.trafficState.ReaderSwitchToDirectCopy = true
|
|
} else {
|
|
errors.LogInfo(w.ctx, "XtlsRead unknown command ", w.trafficState.CurrentCommand, buffer.Len())
|
|
}
|
|
}
|
|
if w.trafficState.NumberOfPacketToFilter > 0 {
|
|
XtlsFilterTls(buffer, w.trafficState, w.ctx)
|
|
}
|
|
}
|
|
return buffer, err
|
|
}
|
|
|
|
// VisionWriter is used to write xtls vision protocol
|
|
// Note Vision probably only make sense as the inner most layer of writer, since it need assess traffic state from origin proxy traffic
|
|
type VisionWriter struct {
|
|
buf.Writer
|
|
trafficState *TrafficState
|
|
ctx context.Context
|
|
writeOnceUserUUID []byte
|
|
}
|
|
|
|
func NewVisionWriter(writer buf.Writer, state *TrafficState, context context.Context) *VisionWriter {
|
|
w := make([]byte, len(state.UserUUID))
|
|
copy(w, state.UserUUID)
|
|
return &VisionWriter{
|
|
Writer: writer,
|
|
trafficState: state,
|
|
ctx: context,
|
|
writeOnceUserUUID: w,
|
|
}
|
|
}
|
|
|
|
func (w *VisionWriter) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
|
if w.trafficState.NumberOfPacketToFilter > 0 {
|
|
XtlsFilterTls(mb, w.trafficState, w.ctx)
|
|
}
|
|
if w.trafficState.IsPadding {
|
|
if len(mb) == 1 && mb[0] == nil {
|
|
mb[0] = XtlsPadding(nil, CommandPaddingContinue, &w.writeOnceUserUUID, true, w.ctx) // we do a long padding to hide vless header
|
|
return w.Writer.WriteMultiBuffer(mb)
|
|
}
|
|
mb = ReshapeMultiBuffer(w.ctx, mb)
|
|
longPadding := w.trafficState.IsTLS
|
|
for i, b := range mb {
|
|
if w.trafficState.IsTLS && b.Len() >= 6 && bytes.Equal(TlsApplicationDataStart, b.BytesTo(3)) {
|
|
if w.trafficState.EnableXtls {
|
|
w.trafficState.WriterSwitchToDirectCopy = true
|
|
}
|
|
var command byte = CommandPaddingContinue
|
|
if i == len(mb)-1 {
|
|
command = CommandPaddingEnd
|
|
if w.trafficState.EnableXtls {
|
|
command = CommandPaddingDirect
|
|
}
|
|
}
|
|
mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, true, w.ctx)
|
|
w.trafficState.IsPadding = false // padding going to end
|
|
longPadding = false
|
|
continue
|
|
} else if !w.trafficState.IsTLS12orAbove && w.trafficState.NumberOfPacketToFilter <= 1 { // For compatibility with earlier vision receiver, we finish padding 1 packet early
|
|
w.trafficState.IsPadding = false
|
|
mb[i] = XtlsPadding(b, CommandPaddingEnd, &w.writeOnceUserUUID, longPadding, w.ctx)
|
|
break
|
|
}
|
|
var command byte = CommandPaddingContinue
|
|
if i == len(mb)-1 && !w.trafficState.IsPadding {
|
|
command = CommandPaddingEnd
|
|
if w.trafficState.EnableXtls {
|
|
command = CommandPaddingDirect
|
|
}
|
|
}
|
|
mb[i] = XtlsPadding(b, command, &w.writeOnceUserUUID, longPadding, w.ctx)
|
|
}
|
|
}
|
|
return w.Writer.WriteMultiBuffer(mb)
|
|
}
|
|
|
|
// ReshapeMultiBuffer prepare multi buffer for padding structure (max 21 bytes)
|
|
func ReshapeMultiBuffer(ctx context.Context, buffer buf.MultiBuffer) buf.MultiBuffer {
|
|
needReshape := 0
|
|
for _, b := range buffer {
|
|
if b.Len() >= buf.Size-21 {
|
|
needReshape += 1
|
|
}
|
|
}
|
|
if needReshape == 0 {
|
|
return buffer
|
|
}
|
|
mb2 := make(buf.MultiBuffer, 0, len(buffer)+needReshape)
|
|
toPrint := ""
|
|
for i, buffer1 := range buffer {
|
|
if buffer1.Len() >= buf.Size-21 {
|
|
index := int32(bytes.LastIndex(buffer1.Bytes(), TlsApplicationDataStart))
|
|
if index < 21 || index > buf.Size-21 {
|
|
index = buf.Size / 2
|
|
}
|
|
buffer2 := buf.New()
|
|
buffer2.Write(buffer1.BytesFrom(index))
|
|
buffer1.Resize(0, index)
|
|
mb2 = append(mb2, buffer1, buffer2)
|
|
toPrint += " " + strconv.Itoa(int(buffer1.Len())) + " " + strconv.Itoa(int(buffer2.Len()))
|
|
} else {
|
|
mb2 = append(mb2, buffer1)
|
|
toPrint += " " + strconv.Itoa(int(buffer1.Len()))
|
|
}
|
|
buffer[i] = nil
|
|
}
|
|
buffer = buffer[:0]
|
|
errors.LogInfo(ctx, "ReshapeMultiBuffer ", toPrint)
|
|
return mb2
|
|
}
|
|
|
|
// XtlsPadding add padding to eliminate length signature during tls handshake
|
|
func XtlsPadding(b *buf.Buffer, command byte, userUUID *[]byte, longPadding bool, ctx context.Context) *buf.Buffer {
|
|
var contentLen int32 = 0
|
|
var paddingLen int32 = 0
|
|
if b != nil {
|
|
contentLen = b.Len()
|
|
}
|
|
if contentLen < 900 && longPadding {
|
|
l, err := rand.Int(rand.Reader, big.NewInt(500))
|
|
if err != nil {
|
|
errors.LogDebugInner(ctx, err, "failed to generate padding")
|
|
}
|
|
paddingLen = int32(l.Int64()) + 900 - contentLen
|
|
} else {
|
|
l, err := rand.Int(rand.Reader, big.NewInt(256))
|
|
if err != nil {
|
|
errors.LogDebugInner(ctx, err, "failed to generate padding")
|
|
}
|
|
paddingLen = int32(l.Int64())
|
|
}
|
|
if paddingLen > buf.Size-21-contentLen {
|
|
paddingLen = buf.Size - 21 - contentLen
|
|
}
|
|
newbuffer := buf.New()
|
|
if userUUID != nil {
|
|
newbuffer.Write(*userUUID)
|
|
*userUUID = nil
|
|
}
|
|
newbuffer.Write([]byte{command, byte(contentLen >> 8), byte(contentLen), byte(paddingLen >> 8), byte(paddingLen)})
|
|
if b != nil {
|
|
newbuffer.Write(b.Bytes())
|
|
b.Release()
|
|
b = nil
|
|
}
|
|
newbuffer.Extend(paddingLen)
|
|
errors.LogInfo(ctx, "XtlsPadding ", contentLen, " ", paddingLen, " ", command)
|
|
return newbuffer
|
|
}
|
|
|
|
// XtlsUnpadding remove padding and parse command
|
|
func XtlsUnpadding(b *buf.Buffer, s *TrafficState, ctx context.Context) *buf.Buffer {
|
|
if s.RemainingCommand == -1 && s.RemainingContent == -1 && s.RemainingPadding == -1 { // inital state
|
|
if b.Len() >= 21 && bytes.Equal(s.UserUUID, b.BytesTo(16)) {
|
|
b.Advance(16)
|
|
s.RemainingCommand = 5
|
|
} else {
|
|
return b
|
|
}
|
|
}
|
|
newbuffer := buf.New()
|
|
for b.Len() > 0 {
|
|
if s.RemainingCommand > 0 {
|
|
data, err := b.ReadByte()
|
|
if err != nil {
|
|
return newbuffer
|
|
}
|
|
switch s.RemainingCommand {
|
|
case 5:
|
|
s.CurrentCommand = int(data)
|
|
case 4:
|
|
s.RemainingContent = int32(data) << 8
|
|
case 3:
|
|
s.RemainingContent = s.RemainingContent | int32(data)
|
|
case 2:
|
|
s.RemainingPadding = int32(data) << 8
|
|
case 1:
|
|
s.RemainingPadding = s.RemainingPadding | int32(data)
|
|
errors.LogInfo(ctx, "Xtls Unpadding new block, content ", s.RemainingContent, " padding ", s.RemainingPadding, " command ", s.CurrentCommand)
|
|
}
|
|
s.RemainingCommand--
|
|
} else if s.RemainingContent > 0 {
|
|
len := s.RemainingContent
|
|
if b.Len() < len {
|
|
len = b.Len()
|
|
}
|
|
data, err := b.ReadBytes(len)
|
|
if err != nil {
|
|
return newbuffer
|
|
}
|
|
newbuffer.Write(data)
|
|
s.RemainingContent -= len
|
|
} else { // remainingPadding > 0
|
|
len := s.RemainingPadding
|
|
if b.Len() < len {
|
|
len = b.Len()
|
|
}
|
|
b.Advance(len)
|
|
s.RemainingPadding -= len
|
|
}
|
|
if s.RemainingCommand <= 0 && s.RemainingContent <= 0 && s.RemainingPadding <= 0 { // this block done
|
|
if s.CurrentCommand == 0 {
|
|
s.RemainingCommand = 5
|
|
} else {
|
|
s.RemainingCommand = -1 // set to initial state
|
|
s.RemainingContent = -1
|
|
s.RemainingPadding = -1
|
|
if b.Len() > 0 { // shouldn't happen
|
|
newbuffer.Write(b.Bytes())
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
b.Release()
|
|
b = nil
|
|
return newbuffer
|
|
}
|
|
|
|
// XtlsFilterTls filter and recognize tls 1.3 and other info
|
|
func XtlsFilterTls(buffer buf.MultiBuffer, trafficState *TrafficState, ctx context.Context) {
|
|
for _, b := range buffer {
|
|
if b == nil {
|
|
continue
|
|
}
|
|
trafficState.NumberOfPacketToFilter--
|
|
if b.Len() >= 6 {
|
|
startsBytes := b.BytesTo(6)
|
|
if bytes.Equal(TlsServerHandShakeStart, startsBytes[:3]) && startsBytes[5] == TlsHandshakeTypeServerHello {
|
|
trafficState.RemainingServerHello = (int32(startsBytes[3])<<8 | int32(startsBytes[4])) + 5
|
|
trafficState.IsTLS12orAbove = true
|
|
trafficState.IsTLS = true
|
|
if b.Len() >= 79 && trafficState.RemainingServerHello >= 79 {
|
|
sessionIdLen := int32(b.Byte(43))
|
|
cipherSuite := b.BytesRange(43+sessionIdLen+1, 43+sessionIdLen+3)
|
|
trafficState.Cipher = uint16(cipherSuite[0])<<8 | uint16(cipherSuite[1])
|
|
} else {
|
|
errors.LogInfo(ctx, "XtlsFilterTls short server hello, tls 1.2 or older? ", b.Len(), " ", trafficState.RemainingServerHello)
|
|
}
|
|
} else if bytes.Equal(TlsClientHandShakeStart, startsBytes[:2]) && startsBytes[5] == TlsHandshakeTypeClientHello {
|
|
trafficState.IsTLS = true
|
|
errors.LogInfo(ctx, "XtlsFilterTls found tls client hello! ", buffer.Len())
|
|
}
|
|
}
|
|
if trafficState.RemainingServerHello > 0 {
|
|
end := trafficState.RemainingServerHello
|
|
if end > b.Len() {
|
|
end = b.Len()
|
|
}
|
|
trafficState.RemainingServerHello -= b.Len()
|
|
if bytes.Contains(b.BytesTo(end), Tls13SupportedVersions) {
|
|
v, ok := Tls13CipherSuiteDic[trafficState.Cipher]
|
|
if !ok {
|
|
v = "Old cipher: " + strconv.FormatUint(uint64(trafficState.Cipher), 16)
|
|
} else if v != "TLS_AES_128_CCM_8_SHA256" {
|
|
trafficState.EnableXtls = true
|
|
}
|
|
errors.LogInfo(ctx, "XtlsFilterTls found tls 1.3! ", b.Len(), " ", v)
|
|
trafficState.NumberOfPacketToFilter = 0
|
|
return
|
|
} else if trafficState.RemainingServerHello <= 0 {
|
|
errors.LogInfo(ctx, "XtlsFilterTls found tls 1.2! ", b.Len())
|
|
trafficState.NumberOfPacketToFilter = 0
|
|
return
|
|
}
|
|
errors.LogInfo(ctx, "XtlsFilterTls inconclusive server hello ", b.Len(), " ", trafficState.RemainingServerHello)
|
|
}
|
|
if trafficState.NumberOfPacketToFilter <= 0 {
|
|
errors.LogInfo(ctx, "XtlsFilterTls stop filtering", buffer.Len())
|
|
}
|
|
}
|
|
}
|
|
|
|
// UnwrapRawConn support unwrap stats, tls, utls, reality and proxyproto conn and get raw tcp conn from it
|
|
func UnwrapRawConn(conn net.Conn) (net.Conn, stats.Counter, stats.Counter) {
|
|
var readCounter, writerCounter stats.Counter
|
|
if conn != nil {
|
|
statConn, ok := conn.(*stat.CounterConnection)
|
|
if ok {
|
|
conn = statConn.Connection
|
|
readCounter = statConn.ReadCounter
|
|
writerCounter = statConn.WriteCounter
|
|
}
|
|
if xc, ok := conn.(*tls.Conn); ok {
|
|
conn = xc.NetConn()
|
|
} else if utlsConn, ok := conn.(*tls.UConn); ok {
|
|
conn = utlsConn.NetConn()
|
|
} else if realityConn, ok := conn.(*reality.Conn); ok {
|
|
conn = realityConn.NetConn()
|
|
} else if realityUConn, ok := conn.(*reality.UConn); ok {
|
|
conn = realityUConn.NetConn()
|
|
}
|
|
if pc, ok := conn.(*proxyproto.Conn); ok {
|
|
conn = pc.Raw()
|
|
// 8192 > 4096, there is no need to process pc's bufReader
|
|
}
|
|
}
|
|
return conn, readCounter, writerCounter
|
|
}
|
|
|
|
// CopyRawConnIfExist use the most efficient copy method.
|
|
// - If caller don't want to turn on splice, do not pass in both reader conn and writer conn
|
|
// - writer are from *transport.Link
|
|
func CopyRawConnIfExist(ctx context.Context, readerConn net.Conn, writerConn net.Conn, writer buf.Writer, timer *signal.ActivityTimer, inTimer *signal.ActivityTimer) error {
|
|
readerConn, readCounter, _ := UnwrapRawConn(readerConn)
|
|
writerConn, _, writeCounter := UnwrapRawConn(writerConn)
|
|
reader := buf.NewReader(readerConn)
|
|
if runtime.GOOS != "linux" && runtime.GOOS != "android" {
|
|
return readV(ctx, reader, writer, timer, readCounter)
|
|
}
|
|
tc, ok := writerConn.(*net.TCPConn)
|
|
if !ok || readerConn == nil || writerConn == nil {
|
|
return readV(ctx, reader, writer, timer, readCounter)
|
|
}
|
|
inbound := session.InboundFromContext(ctx)
|
|
if inbound == nil || inbound.CanSpliceCopy == 3 {
|
|
return readV(ctx, reader, writer, timer, readCounter)
|
|
}
|
|
outbounds := session.OutboundsFromContext(ctx)
|
|
if len(outbounds) == 0 {
|
|
return readV(ctx, reader, writer, timer, readCounter)
|
|
}
|
|
for _, ob := range outbounds {
|
|
if ob.CanSpliceCopy == 3 {
|
|
return readV(ctx, reader, writer, timer, readCounter)
|
|
}
|
|
}
|
|
|
|
for {
|
|
inbound := session.InboundFromContext(ctx)
|
|
outbounds := session.OutboundsFromContext(ctx)
|
|
var splice = inbound.CanSpliceCopy == 1
|
|
for _, ob := range outbounds {
|
|
if ob.CanSpliceCopy != 1 {
|
|
splice = false
|
|
}
|
|
}
|
|
if splice {
|
|
errors.LogInfo(ctx, "CopyRawConn splice")
|
|
statWriter, _ := writer.(*dispatcher.SizeStatWriter)
|
|
//runtime.Gosched() // necessary
|
|
time.Sleep(time.Millisecond) // without this, there will be a rare ssl error for freedom splice
|
|
timer.SetTimeout(8 * time.Hour) // prevent leak, just in case
|
|
if inTimer != nil {
|
|
inTimer.SetTimeout(8 * time.Hour)
|
|
}
|
|
w, err := tc.ReadFrom(readerConn)
|
|
if readCounter != nil {
|
|
readCounter.Add(w) // outbound stats
|
|
}
|
|
if writeCounter != nil {
|
|
writeCounter.Add(w) // inbound stats
|
|
}
|
|
if statWriter != nil {
|
|
statWriter.Counter.Add(w) // user stats
|
|
}
|
|
if err != nil && errors.Cause(err) != io.EOF {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
buffer, err := reader.ReadMultiBuffer()
|
|
if !buffer.IsEmpty() {
|
|
if readCounter != nil {
|
|
readCounter.Add(int64(buffer.Len()))
|
|
}
|
|
timer.Update()
|
|
if werr := writer.WriteMultiBuffer(buffer); werr != nil {
|
|
return werr
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
func readV(ctx context.Context, reader buf.Reader, writer buf.Writer, timer signal.ActivityUpdater, readCounter stats.Counter) error {
|
|
errors.LogInfo(ctx, "CopyRawConn readv")
|
|
if err := buf.Copy(reader, writer, buf.UpdateActivity(timer), buf.AddToStatCounter(readCounter)); err != nil {
|
|
return errors.New("failed to process response").Base(err)
|
|
}
|
|
return nil
|
|
}
|