diff --git a/app/proxyman/inbound/worker.go b/app/proxyman/inbound/worker.go index 1434af078..9268cdff2 100644 --- a/app/proxyman/inbound/worker.go +++ b/app/proxyman/inbound/worker.go @@ -113,7 +113,7 @@ func (w *tcpWorker) Proxy() proxy.Inbound { } func (w *tcpWorker) Start() error { - ctx := context.Background() + ctx := w.ctx proxyEnvironment := envctx.EnvironmentFromContext(w.ctx).(environment.ProxyEnvironment) transportEnvironment, err := proxyEnvironment.NarrowScopeToTransport("transport") if err != nil { diff --git a/common/environment/rootcap_impl.go b/common/environment/rootcap_impl.go index 78d46d7a5..55e01ce19 100644 --- a/common/environment/rootcap_impl.go +++ b/common/environment/rootcap_impl.go @@ -229,7 +229,7 @@ func (t *transportEnvImpl) Listener() internet.SystemListener { } func (t *transportEnvImpl) OutboundDialer() tagged.DialFunc { - panic("implement me") + return tagged.Dialer } func (t *transportEnvImpl) TransientStorage() storage.ScopedTransientStorage { diff --git a/common/protocol/quic/sniff_test.go b/common/protocol/quic/sniff_test.go index 63060c165..6a7e6a4ea 100644 --- a/common/protocol/quic/sniff_test.go +++ b/common/protocol/quic/sniff_test.go @@ -256,4 +256,4 @@ func TestSniffFakeQUICPacketWithTooShortData(t *testing.T) { if err == nil { t.Error("failed") } -} \ No newline at end of file +} diff --git a/main/distro/all/all.go b/main/distro/all/all.go index 2d7eea1d2..25283a82f 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -87,6 +87,8 @@ import ( _ "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" + _ "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/server" + // Transport headers _ "github.com/v2fly/v2ray-core/v5/transport/internet/headers/http" _ "github.com/v2fly/v2ray-core/v5/transport/internet/headers/noop" diff --git a/transport/internet/tlsmirror/interface.go b/transport/internet/tlsmirror/interface.go new file mode 100644 index 000000000..0414ecca7 --- /dev/null +++ b/transport/internet/tlsmirror/interface.go @@ -0,0 +1,46 @@ +package tlsmirror + +import ( + "context" + + "github.com/v2fly/v2ray-core/v5/common" +) + +type TLSRecord struct { + RecordType byte + LegacyProtocolVersion [2]byte + RecordLength uint16 + Fragment []byte +} + +type RecordReader interface { + ReadNextRecord(rejectProfile PartialTLSRecordRejectProfile) (*TLSRecord, error) +} + +type RecordWriter interface { + WriteRecord(record *TLSRecord) error +} + +type Peeker interface { + Peek(n int) ([]byte, error) +} + +type PartialTLSRecordRejectProfile interface { + TestIfReject(record *TLSRecord, readyFields int) error +} + +type MessageHook func(message *TLSRecord) (drop bool, ok error) + +type InsertableTLSConn interface { + common.Closable + GetHandshakeRandom() ([]byte, []byte, error) + InsertC2SMessage(message *TLSRecord) error + InsertS2CMessage(message *TLSRecord) error +} + +const TrafficGeneratorManagedConnectionContextKey = "TrafficGeneratorManagedConnection-ku63HMMD-kduCPhr8-DN4y6WEa" + +type TrafficGeneratorManagedConnection interface { + RecallTrafficGenerator() error + WaitConnectionReady() context.Context +} diff --git a/transport/internet/tlsmirror/mirrorbase/base.go b/transport/internet/tlsmirror/mirrorbase/base.go new file mode 100644 index 000000000..c5231e55d --- /dev/null +++ b/transport/internet/tlsmirror/mirrorbase/base.go @@ -0,0 +1,3 @@ +package mirrorbase + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen diff --git a/transport/internet/tlsmirror/mirrorbase/conn.go b/transport/internet/tlsmirror/mirrorbase/conn.go new file mode 100644 index 000000000..ac9e4e0cd --- /dev/null +++ b/transport/internet/tlsmirror/mirrorbase/conn.go @@ -0,0 +1,342 @@ +package mirrorbase + +import ( + "bufio" + "bytes" + "context" + "io" + "net" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcommon" +) + +// NewMirroredTLSConn creates a new mirrored TLS connection. +// No stable interface +func NewMirroredTLSConn(ctx context.Context, clientConn net.Conn, serverConn net.Conn, onC2SMessage, onS2CMessage tlsmirror.MessageHook, closable common.Closable) tlsmirror.InsertableTLSConn { + c := &conn{ + ctx: ctx, + clientConn: clientConn, + serverConn: serverConn, + c2sInsert: make(chan *tlsmirror.TLSRecord, 100), + s2cInsert: make(chan *tlsmirror.TLSRecord, 100), + OnC2SMessage: onC2SMessage, + OnS2CMessage: onS2CMessage, + } + c.ctx, c.done = context.WithCancel(ctx) + go c.c2sWorker() + go c.s2cWorker() + go func() { + <-c.ctx.Done() + if closable != nil { + closable.Close() + } + c.clientConn.Close() + c.serverConn.Close() + }() + return c +} + +type conn struct { + ctx context.Context + done context.CancelFunc + + clientConn net.Conn + serverConn net.Conn + + OnC2SMessage tlsmirror.MessageHook + OnS2CMessage tlsmirror.MessageHook + + c2sInsert chan *tlsmirror.TLSRecord + s2cInsert chan *tlsmirror.TLSRecord + + isClientRandomReady bool + ClientRandom [32]byte + isServerRandomReady bool + ServerRandom [32]byte +} + +func (c *conn) GetHandshakeRandom() ([]byte, []byte, error) { + if !c.isClientRandomReady || !c.isServerRandomReady { + return nil, nil, newError("client random or server random not ready") + } + return c.ClientRandom[:], c.ServerRandom[:], nil +} + +func (c *conn) Close() error { + c.done() + return nil +} + +func (c *conn) InsertC2SMessage(message *tlsmirror.TLSRecord) error { + duplicatedRecord := mirrorcommon.DuplicateRecord(*message) + c.c2sInsert <- &duplicatedRecord + return nil +} + +func (c *conn) InsertS2CMessage(message *tlsmirror.TLSRecord) error { + duplicatedRecord := mirrorcommon.DuplicateRecord(*message) + c.s2cInsert <- &duplicatedRecord + return nil +} + +type bufPeeker struct { + buffer []byte +} + +func (b *bufPeeker) Peek(n int) ([]byte, error) { + if len(b.buffer) < n { + return nil, newError("not enough data") + } + return b.buffer[:n], nil +} + +type readerWithInitialData struct { + initialData []byte + innerReader io.Reader +} + +func (r *readerWithInitialData) initialDataDrained() bool { + return len(r.initialData) == 0 +} + +func (r *readerWithInitialData) Read(p []byte) (n int, err error) { + if len(r.initialData) > 0 { + n = copy(p, r.initialData) + r.initialData = r.initialData[n:] + return n, nil + } + return r.innerReader.Read(p) +} + +func (c *conn) c2sWorker() { + c2sHandshake, handshakeReminder, c2sReminderData, err := c.captureHandshake(c.clientConn, c.serverConn) + if err != nil { + c.done() + return + } + _ = c2sHandshake + _ = handshakeReminder + _ = c2sReminderData + serverConnectionWriter := bufio.NewWriter(c.serverConn) + _, err = io.Copy(serverConnectionWriter, bytes.NewReader(handshakeReminder)) + if err != nil { + c.done() + newError("failed to copy handshake reminder").Base(err).AtWarning().WriteToLog() + return + } + + clientHello, err := mirrorcommon.UnpackTLSClientHello(c2sHandshake.Fragment) + if err != nil { + c.done() + newError("failed to unpack client hello").Base(err).AtWarning().WriteToLog() + return + } + c.ClientRandom = clientHello.ClientRandom + c.isClientRandomReady = true + + clientSocketReader := &readerWithInitialData{initialData: c2sReminderData, innerReader: c.clientConn} + clientSocket := bufio.NewReaderSize(clientSocketReader, 65536) + + recordReader := mirrorcommon.NewTLSRecordStreamReader(clientSocket) + recordWriter := mirrorcommon.NewTLSRecordStreamWriter(serverConnectionWriter) + if len(c2sReminderData) == 0 { + err := serverConnectionWriter.Flush() + if err != nil { + c.done() + newError("failed to flush server connection writer").Base(err).AtWarning().WriteToLog() + return + } + } + go func() { + for c.ctx.Err() == nil { + record := <-c.c2sInsert + err := recordWriter.WriteRecord(record, false) + if err != nil { + c.done() + newError("failed to write C2S message").Base(err).AtWarning().WriteToLog() + return + } + } + }() + for c.ctx.Err() == nil { + record, err := recordReader.ReadNextRecord() + if err != nil { + drainCopy(c.clientConn, nil, c.serverConn) + c.done() + newError("failed to read TLS record").Base(err).AtWarning().WriteToLog() + return + } + if c.OnC2SMessage != nil { + drop, err := c.OnC2SMessage(record) + if err != nil { + c.done() + newError("failed to process C2S message").Base(err).AtWarning().WriteToLog() + return + } + if drop { + continue + } + } + duplicatedRecord := mirrorcommon.DuplicateRecord(*record) + c.c2sInsert <- &duplicatedRecord + } +} + +func (c *conn) s2cWorker() { + // TODO: stick packets together, if they arrived so + s2cHandshake, handshakeReminder, s2cReminderData, err := c.captureHandshake(c.serverConn, c.clientConn) + if err != nil { + c.done() + return + } + _ = s2cHandshake + _ = handshakeReminder + _ = s2cReminderData + + clientConnectionWriter := bufio.NewWriter(c.clientConn) + + _, err = io.Copy(clientConnectionWriter, bytes.NewReader(handshakeReminder)) + if err != nil { + c.done() + newError("failed to copy handshake reminder").Base(err).AtWarning().WriteToLog() + return + } + + serverHello, err := mirrorcommon.UnpackTLSServerHello(s2cHandshake.Fragment) + if err != nil { + c.done() + newError("failed to unpack server hello").Base(err).AtWarning().WriteToLog() + return + } + c.ServerRandom = serverHello.ServerRandom + c.isServerRandomReady = true + + serverSocketReader := &readerWithInitialData{initialData: s2cReminderData, innerReader: c.serverConn} + serverSocket := bufio.NewReaderSize(serverSocketReader, 65536) + recordReader := mirrorcommon.NewTLSRecordStreamReader(serverSocket) + recordWriter := mirrorcommon.NewTLSRecordStreamWriter(clientConnectionWriter) + + if len(s2cReminderData) == 0 { + err := clientConnectionWriter.Flush() + if err != nil { + c.done() + newError("failed to flush client connection writer").Base(err).AtWarning().WriteToLog() + return + } + } + go func() { + for c.ctx.Err() == nil { + record := <-c.s2cInsert + err := recordWriter.WriteRecord(record, false) + if err != nil { + c.done() + newError("failed to write S2C message").Base(err).AtWarning().WriteToLog() + return + } + } + }() + for c.ctx.Err() == nil { + record, err := recordReader.ReadNextRecord() + if err != nil { + drainCopy(c.clientConn, nil, c.serverConn) + c.done() + newError("failed to read TLS record").Base(err).AtWarning().WriteToLog() + return + } + if c.OnS2CMessage != nil { + drop, err := c.OnS2CMessage(record) + if err != nil { + c.done() + newError("failed to process S2C message").Base(err).AtWarning().WriteToLog() + return + } + if drop { + continue + } + } + duplicatedRecord := mirrorcommon.DuplicateRecord(*record) + c.s2cInsert <- &duplicatedRecord + } +} + +func drainCopy(dst io.Writer, initData []byte, src io.Reader) { + if initData != nil { + _, err := io.Copy(dst, bytes.NewReader(initData)) + if err != nil { + newError("failed to drain copy").Base(err).AtWarning().WriteToLog() + } + } + _, err := io.Copy(dst, src) + if err != nil { + newError("failed to drain copy").Base(err).AtWarning().WriteToLog() + } +} + +type rejectionDecisionMaker struct{} + +func (r *rejectionDecisionMaker) TestIfReject(record *tlsmirror.TLSRecord, readyFields int) error { + if readyFields >= 1 { + if record.RecordType != mirrorcommon.TLSRecord_RecordType_handshake { + return newError("unexpected record type").AtWarning() + } + } + if readyFields >= 2 { + switch record.LegacyProtocolVersion[0] { + case 0x01: + case 0x02: + case 0x03: + if record.LegacyProtocolVersion[1] > 0x03 { + return newError("unexpected minor protocol version").AtWarning() + } + default: + return newError("unexpected major protocol version").AtWarning() + } + } + return nil +} + +func (c *conn) captureHandshake(sourceConn, mirrorConn net.Conn) (handshake tlsmirror.TLSRecord, handshakeReminder, rest []byte, reterr error) { + var readBuffer [65536]byte + var nextRead int + for c.ctx.Err() == nil { + n, err := sourceConn.Read(readBuffer[nextRead:]) + if err != nil { + c.done() + reterr = newError("failed to read from source connection").Base(err).AtWarning() + return handshake, nil, nil, reterr + } + handshakeRejectionDecisionMaker := &rejectionDecisionMaker{} + result, tryAgainLen, processed, err := mirrorcommon.PeekTLSRecord(&bufPeeker{buffer: readBuffer[:nextRead+n]}, handshakeRejectionDecisionMaker) + if processed == 0 { + if tryAgainLen == 0 { + // TODO: directly copy + drainCopy(mirrorConn, readBuffer[:nextRead+n], sourceConn) + c.done() + reterr = newError("failed to peek tls record").Base(err).AtWarning() + return handshake, nil, nil, reterr + } + _, err = io.Copy(mirrorConn, bytes.NewReader(readBuffer[nextRead:nextRead+n])) + if err != nil { + c.done() + newError("failed to copy to server connection").Base(err).AtWarning().WriteToLog() + return handshake, nil, nil, reterr + } + nextRead += n + } else { + // Parse the client hello + if result.RecordType != mirrorcommon.TLSRecord_RecordType_handshake { + c.done() + reterr = newError("unexpected record type").AtWarning() + return handshake, nil, nil, reterr + } + handshake = result + handshakeReminder = readBuffer[nextRead:processed] + rest = readBuffer[processed : nextRead+n] + return handshake, handshakeReminder, rest, nil + } + } + reterr = newError("context is done") + return handshake, nil, nil, reterr +} diff --git a/transport/internet/tlsmirror/mirrorbase/errors.generated.go b/transport/internet/tlsmirror/mirrorbase/errors.generated.go new file mode 100644 index 000000000..a4574d81f --- /dev/null +++ b/transport/internet/tlsmirror/mirrorbase/errors.generated.go @@ -0,0 +1,9 @@ +package mirrorbase + +import "github.com/v2fly/v2ray-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/tlsmirror/mirrorcommon/handshake.go b/transport/internet/tlsmirror/mirrorcommon/handshake.go new file mode 100644 index 000000000..c0c455660 --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcommon/handshake.go @@ -0,0 +1,40 @@ +package mirrorcommon + +import ( + "bytes" + + "github.com/v2fly/struc" +) + +type TLSClientHello struct { + HandshakeType uint8 + Length [3]byte + Version uint16 + ClientRandom [32]byte + + // There are other entries, however we do not need them yet +} + +func UnpackTLSClientHello(data []byte) (TLSClientHello, error) { + var clientHello TLSClientHello + err := struc.Unpack(bytes.NewReader(data), &clientHello) + return clientHello, err +} + +type TLSServerHello struct { + HandshakeType uint8 + Length [3]byte + Version uint16 + ServerRandom [32]byte + SessionIDLength uint8 `struc:"sizeof=SessionID"` + SessionID []byte + CipherSuite uint16 + + // There are other entries, however we do not need them yet +} + +func UnpackTLSServerHello(data []byte) (TLSServerHello, error) { + var serverHello TLSServerHello + err := struc.Unpack(bytes.NewReader(data), &serverHello) + return serverHello, err +} diff --git a/transport/internet/tlsmirror/mirrorcommon/record.go b/transport/internet/tlsmirror/mirrorcommon/record.go new file mode 100644 index 000000000..c6b9715b0 --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcommon/record.go @@ -0,0 +1,61 @@ +package mirrorcommon + +import ( + "fmt" + + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" +) + +// PeekTLSRecord reads a TLS record from peeker. +// It returns the record, the number of bytes read from peeker, and any error encountered. +// It does not consume content from peeker, and the content peeked is borrowed from peeker. +func PeekTLSRecord(peeker tlsmirror.Peeker, rejectionProfile tlsmirror.PartialTLSRecordRejectProfile) (result tlsmirror.TLSRecord, tryAgainLength, processed int, err error) { + var record tlsmirror.TLSRecord + header, err := peeker.Peek(5) + if err != nil { + return record, 0, 0, err + } + if len(header) < 5 { + return record, 5, 0, fmt.Errorf("tls: record header too short") + } + record.RecordType = header[0] + record.LegacyProtocolVersion[0] = header[1] + record.LegacyProtocolVersion[1] = header[2] + record.RecordLength = uint16(header[3])<<8 | uint16(header[4]) + if record.RecordLength > 16384 { + return record, 0, 0, fmt.Errorf("tls: record length %d is too large", record.RecordLength) + } + if rejectionProfile != nil { + err = rejectionProfile.TestIfReject(&record, 2) + if err != nil { + return record, 0, 0, err + } + } + fragment, err := peeker.Peek(int(5 + record.RecordLength)) + if err != nil { + return record, int(5 + record.RecordLength), 0, err + } + if len(fragment) < 5+int(record.RecordLength) { + return record, int(5 + record.RecordLength), 0, fmt.Errorf("tls: record fragment too short") + } + record.Fragment = fragment[5:] + return record, 0, int(5 + record.RecordLength), nil +} + +func PackTLSRecord(record tlsmirror.TLSRecord) []byte { + buf := make([]byte, 5+len(record.Fragment)) + buf[0] = record.RecordType + buf[1] = record.LegacyProtocolVersion[0] + buf[2] = record.LegacyProtocolVersion[1] + buf[3] = byte(record.RecordLength >> 8) + buf[4] = byte(record.RecordLength) + copy(buf[5:], record.Fragment) + return buf +} + +func DuplicateRecord(record tlsmirror.TLSRecord) tlsmirror.TLSRecord { + newRecord := record + newRecord.Fragment = make([]byte, len(record.Fragment)) + copy(newRecord.Fragment, record.Fragment) + return newRecord +} diff --git a/transport/internet/tlsmirror/mirrorcommon/record_consts.go b/transport/internet/tlsmirror/mirrorcommon/record_consts.go new file mode 100644 index 000000000..424acd95a --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcommon/record_consts.go @@ -0,0 +1,6 @@ +package mirrorcommon + +const ( + TLSRecord_RecordType_application_data = 23 + TLSRecord_RecordType_handshake = 22 +) diff --git a/transport/internet/tlsmirror/mirrorcommon/recordstream.go b/transport/internet/tlsmirror/mirrorcommon/recordstream.go new file mode 100644 index 000000000..195520f8c --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcommon/recordstream.go @@ -0,0 +1,73 @@ +package mirrorcommon + +import ( + "bufio" + "errors" + "io" + + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" +) + +func NewTLSRecordStreamReader(reader *bufio.Reader) *TLSRecordStreamReader { + return &TLSRecordStreamReader{bufferedReader: reader} +} + +type TLSRecordStreamReader struct { + bufferedReader *bufio.Reader + consumedSize int64 +} + +func (t *TLSRecordStreamReader) ReadNextRecord() (*tlsmirror.TLSRecord, error) { + record, tryAgainLength, processedLength, err := PeekTLSRecord(t.bufferedReader, nil) + if err == nil { + _, err := t.bufferedReader.Discard(processedLength) + t.consumedSize += int64(processedLength) + if err != nil { + return nil, err + } + return &record, nil + } + + if errors.Is(err, io.EOF) { + return nil, err + } else { // nolint: gocritic + if tryAgainLength == 0 { + return nil, err + } + } + + if tryAgainLength > 0 { + _, err := t.bufferedReader.Read(make([]byte, tryAgainLength)) + if err != nil { + return nil, err + } + err = t.bufferedReader.UnreadByte() + if err != nil { + return nil, err + } + } + return t.ReadNextRecord() +} + +func (t *TLSRecordStreamReader) GetConsumedSize() int64 { + return t.consumedSize +} + +func NewTLSRecordStreamWriter(writer *bufio.Writer) *TLSRecordStreamWriter { + return &TLSRecordStreamWriter{bufferedWriter: writer} +} + +type TLSRecordStreamWriter struct { + bufferedWriter *bufio.Writer +} + +func (t *TLSRecordStreamWriter) WriteRecord(record *tlsmirror.TLSRecord, holdFlush bool) error { + _, err := t.bufferedWriter.Write(PackTLSRecord(*record)) + if err != nil { + return err + } + if holdFlush { + return nil + } + return t.bufferedWriter.Flush() +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/decryptor.go b/transport/internet/tlsmirror/mirrorcrypto/decryptor.go new file mode 100644 index 000000000..d0a4bbd4d --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/decryptor.go @@ -0,0 +1,37 @@ +package mirrorcrypto + +import ( + "crypto/cipher" + + "github.com/v2fly/v2ray-core/v5/common/crypto" +) + +type Decryptor struct { + nonceGenerator crypto.BytesGenerator + aead cipher.AEAD + nextNonce []byte +} + +func NewDecryptor(encryptionKey []byte, nonceMask []byte) *Decryptor { + wrappedAead := aeadAESGCMTLS13(encryptionKey, nonceMask) + return &Decryptor{ + nonceGenerator: generateInitialAEADNonce(), + aead: wrappedAead, + } +} + +func (d *Decryptor) Open(dst, src []byte) ([]byte, error) { + if d.nextNonce == nil { + d.nextNonce = d.nonceGenerator() + } + dst, err := d.aead.Open(dst[:0], d.nextNonce, src, nil) + if err != nil { + return nil, err + } + d.nextNonce = nil + return dst, nil +} + +func (d *Decryptor) NonceSize() int { + return d.aead.NonceSize() +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/derive_key.go b/transport/internet/tlsmirror/mirrorcrypto/derive_key.go new file mode 100644 index 000000000..6764b2f47 --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/derive_key.go @@ -0,0 +1,34 @@ +package mirrorcrypto + +import ( + "crypto/hkdf" + "crypto/sha256" +) + +func DeriveEncryptionKey(primaryKey, clientRandom, serverRandom []byte, tag string) ([]byte, []byte, error) { + if len(primaryKey) != 32 { + return nil, nil, newError("invalid primary key size: ", len(primaryKey)) + } + if len(clientRandom) != 32 { + return nil, nil, newError("invalid client random size: ", len(clientRandom)) + } + if len(serverRandom) != 32 { + return nil, nil, newError("invalid server random size: ", len(serverRandom)) + } + + // Concatenate the primary key, client random, and server random + combined := append(primaryKey, clientRandom...) // nolint: gocritic + combined = append(combined, serverRandom...) + + encryptionKey, err := hkdf.Expand(sha256.New, combined, "v2ray-sp76YMKM-EkGrFUNL-rTJRJMkU:tlsmirror-encryption", 16) + if err != nil { + return nil, nil, newError("unable to derive encryption key").Base(err) + } + + nonceMask, err := hkdf.Expand(sha256.New, combined, "v2ray-sp76YMKM-EkGrFUNL-rTJRJMkU:tlsmirror-noncemask", 12) + if err != nil { + return nil, nil, newError("unable to derive nonce mask").Base(err) + } + + return encryptionKey, nonceMask, nil +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/encrypter.go b/transport/internet/tlsmirror/mirrorcrypto/encrypter.go new file mode 100644 index 000000000..2c313152d --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/encrypter.go @@ -0,0 +1,30 @@ +package mirrorcrypto + +import ( + "crypto/cipher" + + "github.com/v2fly/v2ray-core/v5/common/crypto" +) + +type Encryptor struct { + nonceGenerator crypto.BytesGenerator + aead cipher.AEAD +} + +func NewEncryptor(encryptionKey []byte, nonceMask []byte) *Encryptor { + wrappedAead := aeadAESGCMTLS13(encryptionKey, nonceMask) + return &Encryptor{ + nonceGenerator: generateInitialAEADNonce(), + aead: wrappedAead, + } +} + +func (e *Encryptor) Seal(dst, src []byte) ([]byte, error) { + nonce := e.nonceGenerator() + dst = e.aead.Seal(dst, nonce, src, nil) + return dst, nil +} + +func (e *Encryptor) NonceSize() int { + return e.aead.NonceSize() +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/errors.generated.go b/transport/internet/tlsmirror/mirrorcrypto/errors.generated.go new file mode 100644 index 000000000..a866ed772 --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/errors.generated.go @@ -0,0 +1,9 @@ +package mirrorcrypto + +import "github.com/v2fly/v2ray-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/mirrorcrypto.go b/transport/internet/tlsmirror/mirrorcrypto/mirrorcrypto.go new file mode 100644 index 000000000..195d63622 --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/mirrorcrypto.go @@ -0,0 +1,9 @@ +package mirrorcrypto + +import "github.com/v2fly/v2ray-core/v5/common/crypto" + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +func generateInitialAEADNonce() crypto.BytesGenerator { + return crypto.GenerateIncreasingNonce([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/tls_cipher_suites.go b/transport/internet/tlsmirror/mirrorcrypto/tls_cipher_suites.go new file mode 100644 index 000000000..866c62419 --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/tls_cipher_suites.go @@ -0,0 +1,69 @@ +package mirrorcrypto + +import ( + "crypto/cipher" + + "golang.org/x/crypto/chacha20poly1305" +) + +const ( + aeadNonceLength = 12 +) + +type aead interface { + cipher.AEAD + + // explicitNonceLen returns the number of bytes of explicit nonce + // included in each record. This is eight for older AEADs and + // zero for modern ones. + explicitNonceLen() int +} + +// xorNonceAEAD wraps an AEAD by XORing in a fixed pattern to the nonce +// before each call. +type xorNonceAEAD struct { + nonceMask [aeadNonceLength]byte + aead cipher.AEAD +} + +func (f *xorNonceAEAD) NonceSize() int { return 8 } // 64-bit sequence number +func (f *xorNonceAEAD) Overhead() int { return f.aead.Overhead() } +func (f *xorNonceAEAD) explicitNonceLen() int { return 0 } + +func (f *xorNonceAEAD) Seal(out, nonce, plaintext, additionalData []byte) []byte { + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + result := f.aead.Seal(out, f.nonceMask[:], plaintext, additionalData) + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + + return result +} + +func (f *xorNonceAEAD) Open(out, nonce, ciphertext, additionalData []byte) ([]byte, error) { + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + result, err := f.aead.Open(out, f.nonceMask[:], ciphertext, additionalData) + for i, b := range nonce { + f.nonceMask[4+i] ^= b + } + + return result, err +} + +func aeadChaCha20Poly1305(key, nonceMask []byte) aead { + if len(nonceMask) != aeadNonceLength { + panic("tls: internal error: wrong nonce length") + } + aead, err := chacha20poly1305.New(key) + if err != nil { + panic(err) + } + + ret := &xorNonceAEAD{aead: aead} + copy(ret.nonceMask[:], nonceMask) + return ret +} diff --git a/transport/internet/tlsmirror/mirrorcrypto/tls_cipher_suites_linkname.go b/transport/internet/tlsmirror/mirrorcrypto/tls_cipher_suites_linkname.go new file mode 100644 index 000000000..6d8a9967e --- /dev/null +++ b/transport/internet/tlsmirror/mirrorcrypto/tls_cipher_suites_linkname.go @@ -0,0 +1,11 @@ +package mirrorcrypto + +import ( + "crypto/cipher" + _ "unsafe" +) + +// This linkname is necessary to avoid duplicating too many internal packages. + +//go:linkname aeadAESGCMTLS13 crypto/tls.aeadAESGCMTLS13 +func aeadAESGCMTLS13(key, nonceMask []byte) cipher.AEAD diff --git a/transport/internet/tlsmirror/server/client.go b/transport/internet/tlsmirror/server/client.go new file mode 100644 index 000000000..5062fbbfd --- /dev/null +++ b/transport/internet/tlsmirror/server/client.go @@ -0,0 +1,235 @@ +package server + +import ( + "context" + + "github.com/golang/protobuf/proto" + + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/environment" + "github.com/v2fly/v2ray-core/v5/common/environment/envctx" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/serial" + "github.com/v2fly/v2ray-core/v5/features/outbound" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorbase" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/tlstrafficgen" +) + +func newPersistentMirrorTLSDialer(ctx context.Context, config *Config, serverAddress net.Destination, overrideSecuritySetting proto.Message) (*persistentMirrorTLSDialer, error) { + persistentDialer := &persistentMirrorTLSDialer{ + ctx: ctx, + serverAddress: serverAddress, + overridingSecuritySettings: overrideSecuritySetting, + } + + err := persistentDialer.init(ctx, config) + if err != nil { + return nil, newError("failed to initialize persistent mirror TLS dialer").Base(err) + } + + return persistentDialer, nil +} + +type persistentMirrorTLSDialer struct { + ctx context.Context + + config *Config + + requestNewConnection func(ctx context.Context) error + incomingConnections chan net.Conn + + listener *OutboundListener + outbound *Outbound + + serverAddress net.Destination + overridingSecuritySettings proto.Message + + trafficGenerator *tlstrafficgen.TrafficGenerator + + obm outbound.Manager +} + +func (d *persistentMirrorTLSDialer) init(ctx context.Context, config *Config) error { + if err := core.RequireFeatures(ctx, func(om outbound.Manager) { + d.obm = om + }); err != nil { + return err + } + + d.requestNewConnection = func(ctx context.Context) error { + return nil + } + + d.ctx = ctx + d.config = config + + d.incomingConnections = make(chan net.Conn, 4) + d.listener = NewOutboundListener() + d.outbound = NewOutbound(d.config.CarrierConnectionTag, d.listener) + + go func() { + err := d.outbound.Start() + if err != nil { + newError("failed to start outbound listener").Base(err).AtWarning().WriteToLog() + return + } + + if err := d.obm.RemoveHandler(context.Background(), d.config.CarrierConnectionTag); err != nil { + newError("failed to remove existing handler").WriteToLog() + } + + err = d.obm.AddHandler(context.Background(), &Outbound{ + tag: d.config.CarrierConnectionTag, + listener: d.listener, + }) + if err != nil { + newError("failed to add outbound handler").Base(err).AtWarning().WriteToLog() + return + } + + for { + var ctx context.Context + conn, err := d.listener.Accept() + if err != nil { + break + } + if ctxGetter, ok := conn.(connectionContextGetter); ok { + ctx = ctxGetter.GetConnectionContext() + } else { + ctx = d.ctx + newError("connection does not implement connectionContextGetter, using default context").AtError().WriteToLog() + } + d.handleIncomingCarrierConnection(ctx, conn) + } + }() + + if d.config.EmbeddedTrafficGenerator != nil { + if d.overridingSecuritySettings != nil && d.config.EmbeddedTrafficGenerator.SecuritySettings == nil { + d.config.EmbeddedTrafficGenerator.SecuritySettings = serial.ToTypedMessage(d.overridingSecuritySettings) + } + d.trafficGenerator = tlstrafficgen.NewTrafficGenerator(d.ctx, d.config.EmbeddedTrafficGenerator, + d.serverAddress, d.config.CarrierConnectionTag) + + d.requestNewConnection = func(ctx context.Context) error { + go func() { + err := d.trafficGenerator.GenerateNextTraffic(d.ctx) + if err != nil { + newError("failed to generate next traffic").Base(err).AtWarning().WriteToLog() + } else { + newError("traffic generation request sent").AtDebug().WriteToLog() + } + }() + return nil + } + } + return nil +} + +func (d *persistentMirrorTLSDialer) handleIncomingCarrierConnection(ctx context.Context, conn net.Conn) { + transportEnvironment := envctx.EnvironmentFromContext(d.ctx).(environment.TransportEnvironment) + dialer := transportEnvironment.OutboundDialer() + + forwardConn, err := dialer(d.ctx, d.serverAddress, d.config.ForwardTag) + if err != nil { + newError("failed to dial to destination").Base(err).AtWarning().WriteToLog() + return + } + + ctx, cancel := context.WithCancel(ctx) + cconnState := &clientConnState{ + ctx: ctx, + done: cancel, + localAddr: conn.LocalAddr(), + remoteAddr: conn.RemoteAddr(), + handler: d.handleIncomingReadyConnection, + primaryKey: d.config.PrimaryKey, + readPipe: make(chan []byte, 1), + } + + cconnState.mirrorConn = mirrorbase.NewMirroredTLSConn(ctx, conn, forwardConn, cconnState.onC2SMessage, cconnState.onS2CMessage, conn) +} + +type connectionContextGetter interface { + GetConnectionContext() context.Context +} + +func (d *persistentMirrorTLSDialer) handleIncomingReadyConnection(conn internet.Connection) { + go func() { + var waitedForReady bool + if getter, ok := conn.(connectionContextGetter); ok { + ctx := getter.GetConnectionContext() + + if managedConnectionController := ctx.Value(tlsmirror.TrafficGeneratorManagedConnectionContextKey); managedConnectionController != nil { + if controller, ok := managedConnectionController.(tlsmirror.TrafficGeneratorManagedConnection); ok { + select { // nolint: staticcheck + case <-controller.WaitConnectionReady().Done(): + waitedForReady = true + // TODO: connection might become invalid and never ready, handle this case + } + } + } + } + if !waitedForReady { + newError("unable to wait for connection ready, please verify your setup").AtWarning().WriteToLog() + } + d.incomingConnections <- conn + }() +} + +func (d *persistentMirrorTLSDialer) Dial(ctx context.Context, + dest net.Destination, settings *internet.MemoryStreamConfig, +) (internet.Connection, error) { + var recvConn net.Conn + select { + case conn := <-d.incomingConnections: + recvConn = conn + default: + err := d.requestNewConnection(ctx) + if err != nil { + return nil, newError("failed to request new connection").Base(err) + } + select { // nolint: staticcheck + case conn := <-d.incomingConnections: + recvConn = conn + } + } + + if recvConn == nil { + return nil, newError("failed to receive connection") + } + + return recvConn, nil +} + +func Dial(ctx context.Context, dest net.Destination, settings *internet.MemoryStreamConfig) (internet.Connection, error) { + transportEnvironment := envctx.EnvironmentFromContext(ctx).(environment.TransportEnvironment) + dialer, err := transportEnvironment.TransientStorage().Get(ctx, "persistentDialer") + if err != nil { + var securitySetting proto.Message + if settings.SecurityType != "" && settings.SecurityType != "none" { + securitySetting = settings.SecuritySettings.(proto.Message) + } + config := settings.ProtocolSettings.(*Config) + detachedContext := core.ToBackgroundDetachedContext(ctx) + dialer, err = newPersistentMirrorTLSDialer(detachedContext, config, dest, securitySetting) + if err != nil { + return nil, newError("failed to create persistent mirror TLS dialer").Base(err) + } + err = transportEnvironment.TransientStorage().Put(ctx, "persistentDialer", dialer) + if err != nil { + return nil, newError("failed to put persistent dialer").Base(err) + } + } + conn, err := dialer.(*persistentMirrorTLSDialer).Dial(ctx, dest, settings) + if err != nil { + return nil, newError("failed to dial").Base(err) + } + return conn, nil +} + +func init() { + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/tlsmirror/server/client_conn.go b/transport/internet/tlsmirror/server/client_conn.go new file mode 100644 index 000000000..de7ed774e --- /dev/null +++ b/transport/internet/tlsmirror/server/client_conn.go @@ -0,0 +1,164 @@ +package server + +import ( + "bytes" + "context" + gonet "net" + "time" + + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcommon" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcrypto" +) + +type clientConnState struct { + ctx context.Context + done context.CancelFunc + handler internet.ConnHandler + + mirrorConn tlsmirror.InsertableTLSConn + localAddr net.Addr + remoteAddr net.Addr + + activated bool + decryptor *mirrorcrypto.Decryptor + encryptor *mirrorcrypto.Encryptor + + primaryKey []byte + + readPipe chan []byte + readBuffer *bytes.Buffer + + protocolVersion [2]byte +} + +func (s *clientConnState) GetConnectionContext() context.Context { + return s.ctx +} + +func (s *clientConnState) Read(b []byte) (n int, err error) { + if s.readBuffer != nil { + n, _ = s.readBuffer.Read(b) + if n > 0 { + return n, nil + } + s.readBuffer = nil + } + + select { + case <-s.ctx.Done(): + return 0, s.ctx.Err() + case data := <-s.readPipe: + s.readBuffer = bytes.NewBuffer(data) + n, err = s.readBuffer.Read(b) + if err != nil { + return 0, err + } + return n, nil + } +} + +func (s *clientConnState) Write(b []byte) (n int, err error) { + err = s.WriteMessage(b) + if err != nil { + return 0, err + } + n = len(b) + return n, nil +} + +func (s *clientConnState) Close() error { + s.done() + return nil +} + +func (s *clientConnState) LocalAddr() gonet.Addr { + return s.remoteAddr +} + +func (s *clientConnState) RemoteAddr() gonet.Addr { + return s.remoteAddr +} + +func (s *clientConnState) SetDeadline(t time.Time) error { + return nil +} + +func (s *clientConnState) SetReadDeadline(t time.Time) error { + return nil +} + +func (s *clientConnState) SetWriteDeadline(t time.Time) error { + return nil +} + +func (s *clientConnState) onC2SMessage(message *tlsmirror.TLSRecord) (drop bool, ok error) { + if message.RecordType == mirrorcommon.TLSRecord_RecordType_application_data { + if s.decryptor == nil { + clientRandom, serverRandom, err := s.mirrorConn.GetHandshakeRandom() + if err != nil { + newError("failed to get handshake random").Base(err).AtWarning().WriteToLog() + return false, nil + } + + { + encryptionKey, nonceMask, err := mirrorcrypto.DeriveEncryptionKey(s.primaryKey, clientRandom, serverRandom, ":s2c") + if err != nil { + newError("failed to derive C2S encryption key").Base(err).AtWarning().WriteToLog() + return false, nil + } + s.decryptor = mirrorcrypto.NewDecryptor(encryptionKey, nonceMask) + } + + { + encryptionKey, nonceMask, err := mirrorcrypto.DeriveEncryptionKey(s.primaryKey, clientRandom, serverRandom, ":c2s") + if err != nil { + newError("failed to derive S2C encryption key").Base(err).AtWarning().WriteToLog() + return false, nil + } + s.encryptor = mirrorcrypto.NewEncryptor(encryptionKey, nonceMask) + } + s.protocolVersion = message.LegacyProtocolVersion + + if !s.activated { + s.handler(s) + s.activated = true + } + } + } + return false, ok +} + +func (s *clientConnState) onS2CMessage(message *tlsmirror.TLSRecord) (drop bool, ok error) { + if message.RecordType == mirrorcommon.TLSRecord_RecordType_application_data { + if s.encryptor == nil { + return false, nil + } + buffer := make([]byte, 0, len(message.Fragment)-s.encryptor.NonceSize()) + buffer, err := s.decryptor.Open(buffer, message.Fragment) + if err != nil { + return false, nil + } + + s.readPipe <- buffer + return true, nil + } + return false, ok +} + +func (s *clientConnState) WriteMessage(message []byte) error { + buffer := make([]byte, 0, len(message)+s.encryptor.NonceSize()) + buffer, err := s.encryptor.Seal(buffer, message) + if err != nil { + return newError("failed to encrypt message").Base(err) + } + record := tlsmirror.TLSRecord{ + RecordType: mirrorcommon.TLSRecord_RecordType_application_data, + LegacyProtocolVersion: s.protocolVersion, + RecordLength: uint16(len(buffer)), + Fragment: buffer, + } + return s.mirrorConn.InsertC2SMessage(&record) +} diff --git a/transport/internet/tlsmirror/server/config.pb.go b/transport/internet/tlsmirror/server/config.pb.go new file mode 100644 index 000000000..942a4fb55 --- /dev/null +++ b/transport/internet/tlsmirror/server/config.pb.go @@ -0,0 +1,169 @@ +package server + +import ( + _ "github.com/v2fly/v2ray-core/v5/common/protoext" + tlstrafficgen "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/tlstrafficgen" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ForwardAddress string `protobuf:"bytes,1,opt,name=forward_address,json=forwardAddress,proto3" json:"forward_address,omitempty"` + ForwardPort uint32 `protobuf:"varint,2,opt,name=forward_port,json=forwardPort,proto3" json:"forward_port,omitempty"` + ForwardTag string `protobuf:"bytes,3,opt,name=forward_tag,json=forwardTag,proto3" json:"forward_tag,omitempty"` + CarrierConnectionTag string `protobuf:"bytes,4,opt,name=carrier_connection_tag,json=carrierConnectionTag,proto3" json:"carrier_connection_tag,omitempty"` + EmbeddedTrafficGenerator *tlstrafficgen.Config `protobuf:"bytes,5,opt,name=embedded_traffic_generator,json=embeddedTrafficGenerator,proto3" json:"embedded_traffic_generator,omitempty"` + PrimaryKey []byte `protobuf:"bytes,6,opt,name=primary_key,json=primaryKey,proto3" json:"primary_key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_tlsmirror_server_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tlsmirror_server_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_tlsmirror_server_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Config) GetForwardAddress() string { + if x != nil { + return x.ForwardAddress + } + return "" +} + +func (x *Config) GetForwardPort() uint32 { + if x != nil { + return x.ForwardPort + } + return 0 +} + +func (x *Config) GetForwardTag() string { + if x != nil { + return x.ForwardTag + } + return "" +} + +func (x *Config) GetCarrierConnectionTag() string { + if x != nil { + return x.CarrierConnectionTag + } + return "" +} + +func (x *Config) GetEmbeddedTrafficGenerator() *tlstrafficgen.Config { + if x != nil { + return x.EmbeddedTrafficGenerator + } + return nil +} + +func (x *Config) GetPrimaryKey() []byte { + if x != nil { + return x.PrimaryKey + } + return nil +} + +var File_transport_internet_tlsmirror_server_config_proto protoreflect.FileDescriptor + +const file_transport_internet_tlsmirror_server_config_proto_rawDesc = "" + + "\n" + + "0transport/internet/tlsmirror/server/config.proto\x12.v2ray.core.transport.internet.tlsmirror.server\x1a common/protoext/extensions.proto\x1a7transport/internet/tlsmirror/tlstrafficgen/config.proto\"\xf6\x02\n" + + "\x06Config\x12'\n" + + "\x0fforward_address\x18\x01 \x01(\tR\x0eforwardAddress\x12!\n" + + "\fforward_port\x18\x02 \x01(\rR\vforwardPort\x12\x1f\n" + + "\vforward_tag\x18\x03 \x01(\tR\n" + + "forwardTag\x124\n" + + "\x16carrier_connection_tag\x18\x04 \x01(\tR\x14carrierConnectionTag\x12{\n" + + "\x1aembedded_traffic_generator\x18\x05 \x01(\v2=.v2ray.core.transport.internet.tlsmirror.tlstrafficgen.ConfigR\x18embeddedTrafficGenerator\x12\x1f\n" + + "\vprimary_key\x18\x06 \x01(\fR\n" + + "primaryKey:+\x82\xb5\x18'\n" + + "\ttransport\x12\ttlsmirror\x8a\xff)\ttlsmirror\x90\xff)\x01B\xab\x01\n" + + "2com.v2ray.core.transport.internet.tlsmirror.serverP\x01ZBgithub.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/server\xaa\x02.V2Ray.Core.Transport.Internet.Tlsmirror.Serverb\x06proto3" + +var ( + file_transport_internet_tlsmirror_server_config_proto_rawDescOnce sync.Once + file_transport_internet_tlsmirror_server_config_proto_rawDescData []byte +) + +func file_transport_internet_tlsmirror_server_config_proto_rawDescGZIP() []byte { + file_transport_internet_tlsmirror_server_config_proto_rawDescOnce.Do(func() { + file_transport_internet_tlsmirror_server_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_tlsmirror_server_config_proto_rawDesc), len(file_transport_internet_tlsmirror_server_config_proto_rawDesc))) + }) + return file_transport_internet_tlsmirror_server_config_proto_rawDescData +} + +var file_transport_internet_tlsmirror_server_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_internet_tlsmirror_server_config_proto_goTypes = []any{ + (*Config)(nil), // 0: v2ray.core.transport.internet.tlsmirror.server.Config + (*tlstrafficgen.Config)(nil), // 1: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Config +} +var file_transport_internet_tlsmirror_server_config_proto_depIdxs = []int32{ + 1, // 0: v2ray.core.transport.internet.tlsmirror.server.Config.embedded_traffic_generator:type_name -> v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Config + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_tlsmirror_server_config_proto_init() } +func file_transport_internet_tlsmirror_server_config_proto_init() { + if File_transport_internet_tlsmirror_server_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_tlsmirror_server_config_proto_rawDesc), len(file_transport_internet_tlsmirror_server_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_tlsmirror_server_config_proto_goTypes, + DependencyIndexes: file_transport_internet_tlsmirror_server_config_proto_depIdxs, + MessageInfos: file_transport_internet_tlsmirror_server_config_proto_msgTypes, + }.Build() + File_transport_internet_tlsmirror_server_config_proto = out.File + file_transport_internet_tlsmirror_server_config_proto_goTypes = nil + file_transport_internet_tlsmirror_server_config_proto_depIdxs = nil +} diff --git a/transport/internet/tlsmirror/server/config.proto b/transport/internet/tlsmirror/server/config.proto new file mode 100644 index 000000000..d6ea23f41 --- /dev/null +++ b/transport/internet/tlsmirror/server/config.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package v2ray.core.transport.internet.tlsmirror.server; +option csharp_namespace = "V2Ray.Core.Transport.Internet.Tlsmirror.Server"; +option go_package = "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/server"; +option java_package = "com.v2ray.core.transport.internet.tlsmirror.server"; +option java_multiple_files = true; + +import "common/protoext/extensions.proto"; +import "transport/internet/tlsmirror/tlstrafficgen/config.proto"; + +message Config { + option (v2ray.core.common.protoext.message_opt).type = "transport"; + option (v2ray.core.common.protoext.message_opt).short_name = "tlsmirror"; + option (v2ray.core.common.protoext.message_opt).allow_restricted_mode_load = true; + + option (v2ray.core.common.protoext.message_opt).transport_original_name = "tlsmirror"; + + string forward_address = 1; + uint32 forward_port = 2; + string forward_tag = 3; + + string carrier_connection_tag = 4; + + v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Config embedded_traffic_generator = 5; + + bytes primary_key = 6; +} \ No newline at end of file diff --git a/transport/internet/tlsmirror/server/conn.go b/transport/internet/tlsmirror/server/conn.go new file mode 100644 index 000000000..994f0a979 --- /dev/null +++ b/transport/internet/tlsmirror/server/conn.go @@ -0,0 +1,150 @@ +package server + +import ( + "bytes" + "context" + "net" + "time" + + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcommon" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorcrypto" +) + +type connState struct { + ctx context.Context + done context.CancelFunc + handler internet.ConnHandler + + mirrorConn tlsmirror.InsertableTLSConn + localAddr net.Addr + remoteAddr net.Addr + + activated bool + decryptor *mirrorcrypto.Decryptor + encryptor *mirrorcrypto.Encryptor + + primaryKey []byte + + readPipe chan []byte + readBuffer *bytes.Buffer + + protocolVersion [2]byte +} + +func (s *connState) Read(b []byte) (n int, err error) { + if s.readBuffer != nil { + n, _ = s.readBuffer.Read(b) + if n > 0 { + return n, nil + } + s.readBuffer = nil + } + + select { + case <-s.ctx.Done(): + return 0, s.ctx.Err() + case data := <-s.readPipe: + s.readBuffer = bytes.NewBuffer(data) + n, err = s.readBuffer.Read(b) + if err != nil { + return 0, err + } + return n, nil + } +} + +func (s *connState) Write(b []byte) (n int, err error) { + err = s.WriteMessage(b) + if err != nil { + return 0, err + } + n = len(b) + return n, nil +} + +func (s *connState) LocalAddr() net.Addr { + return s.localAddr +} + +func (s *connState) RemoteAddr() net.Addr { + return s.remoteAddr +} + +func (s *connState) SetDeadline(t time.Time) error { + return nil +} + +func (s *connState) SetReadDeadline(t time.Time) error { + return nil +} + +func (s *connState) SetWriteDeadline(t time.Time) error { + return nil +} + +func (s *connState) Close() error { + s.done() + return nil +} + +func (s *connState) onC2SMessage(message *tlsmirror.TLSRecord) (drop bool, ok error) { + if message.RecordType == mirrorcommon.TLSRecord_RecordType_application_data { + if s.decryptor == nil { + clientRandom, serverRandom, err := s.mirrorConn.GetHandshakeRandom() + if err != nil { + newError("failed to get handshake random").Base(err).AtWarning().WriteToLog() + return false, nil + } + + { + encryptionKey, nonceMask, err := mirrorcrypto.DeriveEncryptionKey(s.primaryKey, clientRandom, serverRandom, ":c2s") + if err != nil { + newError("failed to derive C2S encryption key").Base(err).AtWarning().WriteToLog() + return false, nil + } + s.decryptor = mirrorcrypto.NewDecryptor(encryptionKey, nonceMask) + } + + { + encryptionKey, nonceMask, err := mirrorcrypto.DeriveEncryptionKey(s.primaryKey, clientRandom, serverRandom, ":s2c") + if err != nil { + newError("failed to derive S2C encryption key").Base(err).AtWarning().WriteToLog() + return false, nil + } + s.encryptor = mirrorcrypto.NewEncryptor(encryptionKey, nonceMask) + } + s.protocolVersion = message.LegacyProtocolVersion + } + + buffer := make([]byte, 0, len(message.Fragment)-s.decryptor.NonceSize()) + buffer, err := s.decryptor.Open(buffer, message.Fragment) + if err != nil { + return false, nil + } + + if !s.activated { + s.handler(s) + s.activated = true + } + s.readPipe <- buffer + return true, nil + } + return false, ok +} + +func (s *connState) WriteMessage(message []byte) error { + buffer := make([]byte, 0, len(message)+s.decryptor.NonceSize()) + buffer, err := s.encryptor.Seal(buffer, message) + if err != nil { + return newError("failed to encrypt message").Base(err) + } + record := tlsmirror.TLSRecord{ + RecordType: mirrorcommon.TLSRecord_RecordType_application_data, + LegacyProtocolVersion: s.protocolVersion, + RecordLength: uint16(len(buffer)), + Fragment: buffer, + } + return s.mirrorConn.InsertS2CMessage(&record) +} diff --git a/transport/internet/tlsmirror/server/errors.generated.go b/transport/internet/tlsmirror/server/errors.generated.go new file mode 100644 index 000000000..2047faf9c --- /dev/null +++ b/transport/internet/tlsmirror/server/errors.generated.go @@ -0,0 +1,9 @@ +package server + +import "github.com/v2fly/v2ray-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/tlsmirror/server/hub.go b/transport/internet/tlsmirror/server/hub.go new file mode 100644 index 000000000..1d9e323ce --- /dev/null +++ b/transport/internet/tlsmirror/server/hub.go @@ -0,0 +1,38 @@ +package server + +import ( + "context" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/transportcommon" +) + +func ListenTLSMirror(ctx context.Context, address net.Address, port net.Port, + streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler, +) (internet.Listener, error) { + tlsMirrorSettings := streamSettings.ProtocolSettings.(*Config) + listener, err := transportcommon.ListenWithSecuritySettings(ctx, address, port, streamSettings) + if err != nil { + return nil, newError("failed to listen TLS mirror").Base(err) + } + + tlsMirrorServer := &Server{ctx: ctx, listener: listener, config: tlsMirrorSettings, handler: handler} + + go tlsMirrorServer.accepts() + + return tlsMirrorServer, nil +} + +const protocolName = "tlsmirror" + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, ListenTLSMirror)) +} + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +} diff --git a/transport/internet/tlsmirror/server/outbound.go b/transport/internet/tlsmirror/server/outbound.go new file mode 100644 index 000000000..167d758d0 --- /dev/null +++ b/transport/internet/tlsmirror/server/outbound.go @@ -0,0 +1,131 @@ +package server + +import ( + "context" + "sync" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/signal/done" + "github.com/v2fly/v2ray-core/v5/transport" +) + +func NewOutboundListener() *OutboundListener { + return &OutboundListener{ + buffer: make(chan *connWithContext, 4), + done: done.New(), + } +} + +type connWithContext struct { + net.Conn + ctx context.Context +} + +func (c *connWithContext) GetConnectionContext() context.Context { + return c.ctx +} + +// OutboundListener is a net.Listener for listening gRPC connections. +type OutboundListener struct { + buffer chan *connWithContext + done *done.Instance +} + +func (l *OutboundListener) addWithContext(ctx context.Context, conn net.Conn) { + select { + case l.buffer <- &connWithContext{Conn: conn, ctx: ctx}: + case <-l.done.Wait(): + conn.Close() + default: + conn.Close() + } +} + +// Accept implements net.Listener. +func (l *OutboundListener) Accept() (net.Conn, error) { + select { + case <-l.done.Wait(): + return nil, newError("listen closed") + case c := <-l.buffer: + return c, nil + } +} + +// Close implement net.Listener. +func (l *OutboundListener) Close() error { + common.Must(l.done.Close()) +L: + for { + select { + case c := <-l.buffer: + c.Close() + default: + break L + } + } + return nil +} + +// Addr implements net.Listener. +func (l *OutboundListener) Addr() net.Addr { + return &net.TCPAddr{ + IP: net.IP{0, 0, 0, 0}, + Port: 0, + } +} + +func NewOutbound(tag string, listener *OutboundListener) *Outbound { + return &Outbound{ + tag: tag, + listener: listener, + } +} + +// Outbound is a outbound.Handler that handles gRPC connections. +type Outbound struct { + tag string + listener *OutboundListener + access sync.RWMutex + closed bool +} + +// Dispatch implements outbound.Handler. +func (co *Outbound) Dispatch(ctx context.Context, link *transport.Link) { + co.access.RLock() + + if co.closed { + common.Interrupt(link.Reader) + common.Interrupt(link.Writer) + co.access.RUnlock() + return + } + + closeSignal := done.New() + c := net.NewConnection(net.ConnectionInputMulti(link.Writer), net.ConnectionOutputMulti(link.Reader), net.ConnectionOnClose(closeSignal)) + co.listener.addWithContext(ctx, c) + co.access.RUnlock() + <-closeSignal.Wait() +} + +// Tag implements outbound.Handler. +func (co *Outbound) Tag() string { + return co.tag +} + +// Start implements common.Runnable. +func (co *Outbound) Start() error { + co.access.Lock() + co.closed = false + co.access.Unlock() + return nil +} + +// Close implements common.Closable. +func (co *Outbound) Close() error { + co.access.Lock() + defer co.access.Unlock() + + co.closed = true + return co.listener.Close() +} diff --git a/transport/internet/tlsmirror/server/server.go b/transport/internet/tlsmirror/server/server.go new file mode 100644 index 000000000..022e272ad --- /dev/null +++ b/transport/internet/tlsmirror/server/server.go @@ -0,0 +1,93 @@ +package server + +import ( + "strings" + "time" + + "golang.org/x/net/context" + + "github.com/v2fly/v2ray-core/v5/common/environment" + "github.com/v2fly/v2ray-core/v5/common/environment/envctx" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/mirrorbase" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type Server struct { + config *Config + + listener net.Listener + handler internet.ConnHandler + + ctx context.Context +} + +func (s *Server) process(conn net.Conn) { + transportEnvironment := envctx.EnvironmentFromContext(s.ctx).(environment.TransportEnvironment) + dialer := transportEnvironment.OutboundDialer() + + port, err := net.PortFromInt(s.config.ForwardPort) + if err != nil { + newError("failed to parse port").Base(err).AtWarning().WriteToLog() + return + } + + address := net.ParseAddress(s.config.ForwardAddress) + + dest := net.TCPDestination(address, port) + + forwardConn, err := dialer(s.ctx, dest, s.config.ForwardTag) + if err != nil { + newError("failed to dial to destination").Base(err).AtWarning().WriteToLog() + return + } + + s.accept(conn, forwardConn) +} + +func (s *Server) accepts() { + for { + conn, err := s.listener.Accept() + if err != nil { + errStr := err.Error() + if strings.Contains(errStr, "closed") { + break + } + newError("failed to accepted raw connections").Base(err).AtWarning().WriteToLog() + if strings.Contains(errStr, "too many") { + time.Sleep(time.Millisecond * 500) + } + continue + } + go s.process(conn) + } +} + +func (s *Server) Close() error { + return s.listener.Close() +} + +func (s *Server) Addr() net.Addr { + return s.listener.Addr() +} + +func (s *Server) accept(clientConn net.Conn, serverConn net.Conn) { + ctx, cancel := context.WithCancel(s.ctx) + conn := &connState{ + ctx: ctx, + done: cancel, + localAddr: clientConn.LocalAddr(), + remoteAddr: clientConn.RemoteAddr(), + primaryKey: s.config.PrimaryKey, + handler: s.onIncomingReadyConnection, + readPipe: make(chan []byte, 1), + } + + conn.mirrorConn = mirrorbase.NewMirroredTLSConn(ctx, clientConn, serverConn, conn.onC2SMessage, nil, conn) +} + +func (s *Server) onIncomingReadyConnection(conn internet.Connection) { + go s.handler(conn) +} diff --git a/transport/internet/tlsmirror/tlstrafficgen/config.pb.go b/transport/internet/tlsmirror/tlstrafficgen/config.pb.go new file mode 100644 index 000000000..1ea045c74 --- /dev/null +++ b/transport/internet/tlsmirror/tlstrafficgen/config.pb.go @@ -0,0 +1,383 @@ +package tlstrafficgen + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + anypb "google.golang.org/protobuf/types/known/anypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Header struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Value string `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` + Values []string `protobuf:"bytes,3,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Header) Reset() { + *x = Header{} + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Header) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Header) ProtoMessage() {} + +func (x *Header) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Header.ProtoReflect.Descriptor instead. +func (*Header) Descriptor() ([]byte, []int) { + return file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Header) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Header) GetValue() string { + if x != nil { + return x.Value + } + return "" +} + +func (x *Header) GetValues() []string { + if x != nil { + return x.Values + } + return nil +} + +type TransferCandidate struct { + state protoimpl.MessageState `protogen:"open.v1"` + Weight int32 `protobuf:"varint,1,opt,name=weight,proto3" json:"weight,omitempty"` + GotoLocation int64 `protobuf:"varint,2,opt,name=goto_location,json=gotoLocation,proto3" json:"goto_location,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TransferCandidate) Reset() { + *x = TransferCandidate{} + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TransferCandidate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TransferCandidate) ProtoMessage() {} + +func (x *TransferCandidate) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TransferCandidate.ProtoReflect.Descriptor instead. +func (*TransferCandidate) Descriptor() ([]byte, []int) { + return file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescGZIP(), []int{1} +} + +func (x *TransferCandidate) GetWeight() int32 { + if x != nil { + return x.Weight + } + return 0 +} + +func (x *TransferCandidate) GetGotoLocation() int64 { + if x != nil { + return x.GotoLocation + } + return 0 +} + +type Step struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Host string `protobuf:"bytes,8,opt,name=host,proto3" json:"host,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Method string `protobuf:"bytes,3,opt,name=method,proto3" json:"method,omitempty"` + WaitAtLeast float32 `protobuf:"fixed32,4,opt,name=wait_at_least,json=waitAtLeast,proto3" json:"wait_at_least,omitempty"` + WaitAtMost float32 `protobuf:"fixed32,5,opt,name=wait_at_most,json=waitAtMost,proto3" json:"wait_at_most,omitempty"` + NextStep []*TransferCandidate `protobuf:"bytes,6,rep,name=next_step,json=nextStep,proto3" json:"next_step,omitempty"` + ConnectionReady bool `protobuf:"varint,7,opt,name=connection_ready,json=connectionReady,proto3" json:"connection_ready,omitempty"` + Headers []*Header `protobuf:"bytes,9,rep,name=headers,proto3" json:"headers,omitempty"` + ConnectionRecallExit bool `protobuf:"varint,10,opt,name=connection_recall_exit,json=connectionRecallExit,proto3" json:"connection_recall_exit,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Step) Reset() { + *x = Step{} + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Step) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Step) ProtoMessage() {} + +func (x *Step) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Step.ProtoReflect.Descriptor instead. +func (*Step) Descriptor() ([]byte, []int) { + return file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescGZIP(), []int{2} +} + +func (x *Step) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Step) GetHost() string { + if x != nil { + return x.Host + } + return "" +} + +func (x *Step) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *Step) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *Step) GetWaitAtLeast() float32 { + if x != nil { + return x.WaitAtLeast + } + return 0 +} + +func (x *Step) GetWaitAtMost() float32 { + if x != nil { + return x.WaitAtMost + } + return 0 +} + +func (x *Step) GetNextStep() []*TransferCandidate { + if x != nil { + return x.NextStep + } + return nil +} + +func (x *Step) GetConnectionReady() bool { + if x != nil { + return x.ConnectionReady + } + return false +} + +func (x *Step) GetHeaders() []*Header { + if x != nil { + return x.Headers + } + return nil +} + +func (x *Step) GetConnectionRecallExit() bool { + if x != nil { + return x.ConnectionRecallExit + } + return false +} + +type Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + Steps []*Step `protobuf:"bytes,1,rep,name=steps,proto3" json:"steps,omitempty"` + SecuritySettings *anypb.Any `protobuf:"bytes,2,opt,name=security_settings,json=securitySettings,proto3" json:"security_settings,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Config) Reset() { + *x = Config{} + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Config) ProtoMessage() {} + +func (x *Config) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescGZIP(), []int{3} +} + +func (x *Config) GetSteps() []*Step { + if x != nil { + return x.Steps + } + return nil +} + +func (x *Config) GetSecuritySettings() *anypb.Any { + if x != nil { + return x.SecuritySettings + } + return nil +} + +var File_transport_internet_tlsmirror_tlstrafficgen_config_proto protoreflect.FileDescriptor + +const file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDesc = "" + + "\n" + + "7transport/internet/tlsmirror/tlstrafficgen/config.proto\x125v2ray.core.transport.internet.tlsmirror.tlstrafficgen\x1a\x19google/protobuf/any.proto\"J\n" + + "\x06Header\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value\x12\x16\n" + + "\x06values\x18\x03 \x03(\tR\x06values\"P\n" + + "\x11TransferCandidate\x12\x16\n" + + "\x06weight\x18\x01 \x01(\x05R\x06weight\x12#\n" + + "\rgoto_location\x18\x02 \x01(\x03R\fgotoLocation\"\xc1\x03\n" + + "\x04Step\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x12\n" + + "\x04host\x18\b \x01(\tR\x04host\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12\x16\n" + + "\x06method\x18\x03 \x01(\tR\x06method\x12\"\n" + + "\rwait_at_least\x18\x04 \x01(\x02R\vwaitAtLeast\x12 \n" + + "\fwait_at_most\x18\x05 \x01(\x02R\n" + + "waitAtMost\x12e\n" + + "\tnext_step\x18\x06 \x03(\v2H.v2ray.core.transport.internet.tlsmirror.tlstrafficgen.TransferCandidateR\bnextStep\x12)\n" + + "\x10connection_ready\x18\a \x01(\bR\x0fconnectionReady\x12W\n" + + "\aheaders\x18\t \x03(\v2=.v2ray.core.transport.internet.tlsmirror.tlstrafficgen.HeaderR\aheaders\x124\n" + + "\x16connection_recall_exit\x18\n" + + " \x01(\bR\x14connectionRecallExit\"\x9e\x01\n" + + "\x06Config\x12Q\n" + + "\x05steps\x18\x01 \x03(\v2;.v2ray.core.transport.internet.tlsmirror.tlstrafficgen.StepR\x05steps\x12A\n" + + "\x11security_settings\x18\x02 \x01(\v2\x14.google.protobuf.AnyR\x10securitySettingsB\xc0\x01\n" + + "9com.v2ray.core.transport.internet.tlsmirror.tlstrafficgenP\x01ZIgithub.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/tlstrafficgen\xaa\x025V2Ray.Core.Transport.Internet.Tlsmirror.Tlstrafficgenb\x06proto3" + +var ( + file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescOnce sync.Once + file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescData []byte +) + +func file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescGZIP() []byte { + file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescOnce.Do(func() { + file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDesc), len(file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDesc))) + }) + return file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDescData +} + +var file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_transport_internet_tlsmirror_tlstrafficgen_config_proto_goTypes = []any{ + (*Header)(nil), // 0: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Header + (*TransferCandidate)(nil), // 1: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.TransferCandidate + (*Step)(nil), // 2: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Step + (*Config)(nil), // 3: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Config + (*anypb.Any)(nil), // 4: google.protobuf.Any +} +var file_transport_internet_tlsmirror_tlstrafficgen_config_proto_depIdxs = []int32{ + 1, // 0: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Step.next_step:type_name -> v2ray.core.transport.internet.tlsmirror.tlstrafficgen.TransferCandidate + 0, // 1: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Step.headers:type_name -> v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Header + 2, // 2: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Config.steps:type_name -> v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Step + 4, // 3: v2ray.core.transport.internet.tlsmirror.tlstrafficgen.Config.security_settings:type_name -> google.protobuf.Any + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_transport_internet_tlsmirror_tlstrafficgen_config_proto_init() } +func file_transport_internet_tlsmirror_tlstrafficgen_config_proto_init() { + if File_transport_internet_tlsmirror_tlstrafficgen_config_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDesc), len(file_transport_internet_tlsmirror_tlstrafficgen_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_tlsmirror_tlstrafficgen_config_proto_goTypes, + DependencyIndexes: file_transport_internet_tlsmirror_tlstrafficgen_config_proto_depIdxs, + MessageInfos: file_transport_internet_tlsmirror_tlstrafficgen_config_proto_msgTypes, + }.Build() + File_transport_internet_tlsmirror_tlstrafficgen_config_proto = out.File + file_transport_internet_tlsmirror_tlstrafficgen_config_proto_goTypes = nil + file_transport_internet_tlsmirror_tlstrafficgen_config_proto_depIdxs = nil +} diff --git a/transport/internet/tlsmirror/tlstrafficgen/config.proto b/transport/internet/tlsmirror/tlstrafficgen/config.proto new file mode 100644 index 000000000..12762f422 --- /dev/null +++ b/transport/internet/tlsmirror/tlstrafficgen/config.proto @@ -0,0 +1,38 @@ +syntax = "proto3"; + +package v2ray.core.transport.internet.tlsmirror.tlstrafficgen; +option csharp_namespace = "V2Ray.Core.Transport.Internet.Tlsmirror.Tlstrafficgen"; +option go_package = "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror/tlstrafficgen"; +option java_package = "com.v2ray.core.transport.internet.tlsmirror.tlstrafficgen"; +option java_multiple_files = true; + +import "google/protobuf/any.proto"; + +message Header { + string name = 1; + string value = 2; + repeated string values = 3; +} + +message TransferCandidate{ + int32 weight = 1; + int64 goto_location = 2; +} + +message Step { + string name = 1; + string host = 8; + string path = 2; + string method = 3; + float wait_at_least = 4; + float wait_at_most = 5; + repeated TransferCandidate next_step = 6; + bool connection_ready = 7; + repeated Header headers = 9; + bool connection_recall_exit = 10; +} + +message Config { + repeated Step steps = 1; + google.protobuf.Any security_settings = 2; +} \ No newline at end of file diff --git a/transport/internet/tlsmirror/tlstrafficgen/errors.generated.go b/transport/internet/tlsmirror/tlstrafficgen/errors.generated.go new file mode 100644 index 000000000..b34d7092b --- /dev/null +++ b/transport/internet/tlsmirror/tlstrafficgen/errors.generated.go @@ -0,0 +1,9 @@ +package tlstrafficgen + +import "github.com/v2fly/v2ray-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/tlsmirror/tlstrafficgen/trafficgen.go b/transport/internet/tlsmirror/tlstrafficgen/trafficgen.go new file mode 100644 index 000000000..091df5a3d --- /dev/null +++ b/transport/internet/tlsmirror/tlstrafficgen/trafficgen.go @@ -0,0 +1,299 @@ +package tlstrafficgen + +import ( + "bufio" + "context" + cryptoRand "crypto/rand" + "io" + "math/big" + "net/http" + "net/url" + "time" + + "golang.org/x/net/http2" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/environment" + "github.com/v2fly/v2ray-core/v5/common/environment/envctx" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/serial" + "github.com/v2fly/v2ray-core/v5/transport/internet/security" + "github.com/v2fly/v2ray-core/v5/transport/internet/tlsmirror" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type TrafficGenerator struct { + config *Config + ctx context.Context + + destination net.Destination + tag string +} + +func NewTrafficGenerator(ctx context.Context, config *Config, destination net.Destination, tag string) *TrafficGenerator { + return &TrafficGenerator{ + ctx: ctx, + config: config, + destination: destination, + tag: tag, + } +} + +type trafficGeneratorManagedConnectionController struct { + readyCtx context.Context + readyDone context.CancelFunc + + recallCtx context.Context + recallDone context.CancelFunc +} + +func (t *trafficGeneratorManagedConnectionController) WaitConnectionReady() context.Context { + return t.readyCtx +} + +func (t *trafficGeneratorManagedConnectionController) RecallTrafficGenerator() error { + t.recallDone() + return nil +} + +// Copied from https://brandur.org/fragments/crypto-rand-float64, Thanks +func randIntN(max int64) int64 { + nBig, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(max)) + if err != nil { + panic(err) + } + return nBig.Int64() +} + +func randFloat64() float64 { + return float64(randIntN(1<<53)) / (1 << 53) +} + +func (generator *TrafficGenerator) GenerateNextTraffic(ctx context.Context) error { + transportEnvironment := envctx.EnvironmentFromContext(generator.ctx).(environment.TransportEnvironment) + dialer := transportEnvironment.OutboundDialer() + + carrierConnectionReadyCtx, carrierConnectionReadyDone := context.WithCancel(generator.ctx) + carrierConnectionRecallCtx, carrierConnectionRecallDone := context.WithCancel(generator.ctx) + + trafficController := &trafficGeneratorManagedConnectionController{ + readyCtx: carrierConnectionReadyCtx, + readyDone: carrierConnectionReadyDone, + recallCtx: carrierConnectionRecallCtx, + recallDone: carrierConnectionRecallDone, + } + + var trafficControllerIfce tlsmirror.TrafficGeneratorManagedConnection = trafficController + managedConnectionContextValue := context.WithValue(generator.ctx, + tlsmirror.TrafficGeneratorManagedConnectionContextKey, trafficControllerIfce) // nolint:staticcheck + + conn, err := dialer(managedConnectionContextValue, generator.destination, generator.tag) + if err != nil { + return newError("failed to dial to destination").Base(err).AtWarning() + } + tlsConn, err := generator.tlsHandshake(conn) + if err != nil { + return newError("failed to create TLS connection").Base(err).AtWarning() + } + getAlpn, ok := tlsConn.(security.ConnectionApplicationProtocol) + if !ok { + return newError("TLS connection does not support getting ALPN").AtWarning() + } + alpn, err := getAlpn.GetConnectionApplicationProtocol() + if err != nil { + return newError("failed to get ALPN from TLS connection").Base(err).AtWarning() + } + steps := generator.config.Steps + currentStep := 0 + httpRoundTripper, err := newSingleConnectionHTTPTransport(tlsConn, alpn) + if err != nil { + return newError("failed to create HTTP transport").Base(err).AtWarning() + } + for { + if currentStep >= len(steps) { + return tlsConn.Close() + } + + step := steps[currentStep] + + url := url.URL{ + Scheme: "https", + Host: step.Host, + Path: step.Path, + } + + httpReq := &http.Request{Host: url.Hostname(), Method: step.Method, URL: &url} + + if step.Headers != nil { + header := make(http.Header, len(step.Headers)) + for _, v := range step.Headers { + if v.Value != "" { + header.Add(v.Name, v.Value) + } + if v.Values != nil { + for _, value := range v.Values { + header.Add(v.Name, value) + } + } + } + httpReq.Header = header + } + + startTime := time.Now() + + resp, err := httpRoundTripper.RoundTrip(httpReq) + if err != nil { + return newError("failed to send HTTP request").Base(err).AtWarning() + } + _, err = io.Copy(io.Discard, resp.Body) + if err != nil { + return newError("failed to read HTTP response body").Base(err).AtWarning() + } + err = resp.Body.Close() + if err != nil { + return newError("failed to close HTTP response body").Base(err).AtWarning() + } + endTime := time.Now() + + eclipsedTime := endTime.Sub(startTime) + secondToWait := float64(step.WaitAtMost-step.WaitAtLeast)*randFloat64() + float64(step.WaitAtLeast) + if eclipsedTime < time.Duration(secondToWait*float64(time.Second)) { + waitTime := time.Duration(secondToWait*float64(time.Second)) - eclipsedTime + newError("waiting for ", waitTime, " after step ", currentStep).AtDebug().WriteToLog() + waitTimer := time.NewTimer(waitTime) + select { + case <-ctx.Done(): + waitTimer.Stop() + return ctx.Err() + case <-waitTimer.C: + } + } else { + newError("step ", currentStep, " took too long: ", eclipsedTime, ", expected: ", secondToWait, " seconds").AtWarning().WriteToLog() + } + + if step.ConnectionReady { + carrierConnectionReadyDone() + newError("connection ready for payload traffic").AtInfo().WriteToLog() + } + + if step.ConnectionRecallExit { + if carrierConnectionRecallCtx.Err() != nil { + return tlsConn.Close() + } + } + + if step.NextStep == nil { + currentStep++ + } else { + overallWeight := int32(0) + for _, nextStep := range step.NextStep { + overallWeight += nextStep.Weight + } + maxBound := big.NewInt(int64(overallWeight)) + selectionValue, err := cryptoRand.Int(cryptoRand.Reader, maxBound) + if err != nil { + return newError("failed to generate random selection value").Base(err).AtWarning() + } + selectedValue := int32(selectionValue.Int64()) + currentValue := int32(0) + matched := false + for _, nextStep := range step.NextStep { + if currentValue >= selectedValue { + currentStep = int(nextStep.GotoLocation) + matched = true + break + } + currentValue += nextStep.Weight + } + if !matched { + newError("invalid steps jump instruction, check configuration for step ", currentStep).AtError().WriteToLog() + currentStep++ + } + } + } +} + +func (generator *TrafficGenerator) tlsHandshake(conn net.Conn) (security.Conn, error) { + securitySettingInstance, err := serial.GetInstanceOf(generator.config.SecuritySettings) + if err != nil { + return nil, newError("failed to get instance of security settings").Base(err) + } + securityEngine, err := common.CreateObject(generator.ctx, securitySettingInstance) + if err != nil { + return nil, newError("unable to create security engine from security settings").Base(err) + } + securityEngineTyped, ok := securityEngine.(security.Engine) + if !ok { + return nil, newError("type assertion error when create security engine from security settings") + } + + return securityEngineTyped.Client(conn) +} + +type httpRequestTransport interface { + http.RoundTripper +} + +func newHTTPRequestTransportH1(conn net.Conn) httpRequestTransport { + return &httpRequestTransportH1{ + conn: conn, + bufReader: bufio.NewReader(conn), + } +} + +type httpRequestTransportH1 struct { + conn net.Conn + bufReader *bufio.Reader +} + +func (h *httpRequestTransportH1) RoundTrip(req *http.Request) (*http.Response, error) { + req.Proto = "HTTP/1.1" + req.ProtoMajor = 1 + req.ProtoMinor = 1 + + err := req.Write(h.conn) + if err != nil { + return nil, err + } + return http.ReadResponse(h.bufReader, req) +} + +func newHTTPRequestTransportH2(conn net.Conn) httpRequestTransport { + transport := &http2.Transport{} + clientConn, err := transport.NewClientConn(conn) + if err != nil { + return nil + } + return &httpRequestTransportH2{ + transport: transport, + clientConnection: clientConn, + } +} + +type httpRequestTransportH2 struct { + transport *http2.Transport + clientConnection *http2.ClientConn +} + +func (h *httpRequestTransportH2) RoundTrip(request *http.Request) (*http.Response, error) { + request.ProtoMajor = 2 + request.ProtoMinor = 0 + + response, err := h.clientConnection.RoundTrip(request) + if err != nil { + return nil, err + } + return response, nil +} + +func newSingleConnectionHTTPTransport(conn net.Conn, alpn string) (httpRequestTransport, error) { + switch alpn { + case "h2": + return newHTTPRequestTransportH2(conn), nil + case "http/1.1", "": + return newHTTPRequestTransportH1(conn), nil + default: + return nil, newError("unknown alpn: " + alpn).AtWarning() + } +}