diff --git a/common/buf/io.go b/common/buf/io.go index 2a4cd6705..35e02b8be 100644 --- a/common/buf/io.go +++ b/common/buf/io.go @@ -25,6 +25,7 @@ type TimeoutReader interface { // Writer extends io.Writer with MultiBuffer. type Writer interface { // WriteMultiBuffer writes a MultiBuffer into underlying writer. + // Caller relinquish the ownership of MultiBuffer after calling this method. WriteMultiBuffer(MultiBuffer) error } diff --git a/common/crypto/chunk.go b/common/crypto/chunk.go index ec4118452..1bd8e6131 100644 --- a/common/crypto/chunk.go +++ b/common/crypto/chunk.go @@ -10,6 +10,7 @@ import ( // ChunkSizeDecoder is a utility class to decode size value from bytes. type ChunkSizeDecoder interface { + // SizeBytes must be stable, return the same value across all calls SizeBytes() int32 Decode([]byte) (uint16, error) } diff --git a/go.mod b/go.mod index 263cd1b89..275b5f798 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/google/gopacket v1.1.19 github.com/gorilla/websocket v1.5.1 github.com/jhump/protoreflect v1.15.3 + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 github.com/miekg/dns v1.1.57 github.com/mustafaturan/bus v1.0.2 github.com/pelletier/go-toml v1.9.5 @@ -36,6 +37,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h12.io/socks v1.0.3 + lukechampine.com/blake3 v1.2.1 ) require ( @@ -59,7 +61,6 @@ require ( github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/reedsolomon v1.11.7 // indirect github.com/leodido/go-urn v1.2.4 // indirect - github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/mustafaturan/monoton v1.0.0 // indirect github.com/onsi/ginkgo/v2 v2.10.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect @@ -81,3 +82,5 @@ require ( golang.org/x/tools v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect ) + +replace github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 => github.com/xiaokangwang/struc v0.0.0-20231031203518-0e381172f248 diff --git a/go.sum b/go.sum index 1dc168f4c..529c94982 100644 --- a/go.sum +++ b/go.sum @@ -195,8 +195,6 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= -github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -330,6 +328,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs= github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ= +github.com/xiaokangwang/struc v0.0.0-20231031203518-0e381172f248 h1:C1JR/QGoOd2e00yquW+C3M8R88ZZ+9oPh+ehS+mPJjQ= +github.com/xiaokangwang/struc v0.0.0-20231031203518-0e381172f248/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/xtaci/smux v1.5.12/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= github.com/xtaci/smux v1.5.15/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY= @@ -578,4 +578,6 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/main/distro/all/all.go b/main/distro/all/all.go index c64fec522..4475e4b06 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -54,6 +54,8 @@ import ( _ "github.com/v2fly/v2ray-core/v5/proxy/vlite/inbound" _ "github.com/v2fly/v2ray-core/v5/proxy/vlite/outbound" + _ "github.com/v2fly/v2ray-core/v5/proxy/shadowsocks2022" + // Transports _ "github.com/v2fly/v2ray-core/v5/transport/internet/domainsocket" _ "github.com/v2fly/v2ray-core/v5/transport/internet/grpc" diff --git a/proxy/shadowsocks2022/client.go b/proxy/shadowsocks2022/client.go new file mode 100644 index 000000000..207c6044e --- /dev/null +++ b/proxy/shadowsocks2022/client.go @@ -0,0 +1,129 @@ +package shadowsocks2022 + +import ( + "context" + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/buf" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/retry" + "github.com/v2fly/v2ray-core/v5/common/session" + "github.com/v2fly/v2ray-core/v5/common/signal" + "github.com/v2fly/v2ray-core/v5/common/task" + "github.com/v2fly/v2ray-core/v5/transport" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "time" +) + +type Client struct { + config *ClientConfig + ctx context.Context +} + +func (c *Client) Process(ctx context.Context, link *transport.Link, dialer internet.Dialer) error { + outbound := session.OutboundFromContext(ctx) + if outbound == nil || !outbound.Target.IsValid() { + return newError("target not specified") + } + destination := outbound.Target + network := destination.Network + + var conn internet.Connection + + err := retry.ExponentialBackoff(5, 100).On(func() error { + dest := net.TCPDestination(c.config.Address.AsAddress(), net.Port(c.config.Port)) + dest.Network = network + rawConn, err := dialer.Dial(ctx, dest) + if err != nil { + return err + } + conn = rawConn + + return nil + }) + if err != nil { + return newError("failed to find an available destination").AtWarning().Base(err) + } + newError("tunneling request to ", destination, " via ", network, ":", c.config.Address).WriteToLog(session.ExportIDToError(ctx)) + defer conn.Close() + + var keyDerivation = newBLAKE3KeyDerivation() + var method Method + switch c.config.Method { + case "2022-blake3-aes-128-gcm": + method = newAES128GCMMethod() + case "2022-blake3-aes-256-gcm": + method = newAES256GCMMethod() + default: + return newError("unknown method: ", c.config.Method) + } + + effectivePsk := c.config.Psk + + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, time.Minute) + + if network == net.Network_TCP { + request := &TCPRequest{ + keyDerivation: keyDerivation, + method: method, + } + TCPRequestBuffer := buf.New() + defer TCPRequestBuffer.Release() + err = request.EncodeTCPRequestHeader(effectivePsk, c.config.Ipsk, destination.Address, + int(destination.Port), nil, TCPRequestBuffer) + if err != nil { + return newError("failed to encode TCP request header").Base(err) + } + _, err = conn.Write(TCPRequestBuffer.Bytes()) + if err != nil { + return newError("failed to write TCP request header").Base(err) + } + requestDone := func() error { + encodedWriter := request.CreateClientC2SWriter(conn) + return buf.Copy(link.Reader, encodedWriter, buf.UpdateActivity(timer)) + } + responseDone := func() error { + err = request.DecodeTCPResponseHeader(effectivePsk, conn) + if err != nil { + return newError("failed to decode TCP response header").Base(err) + } + if err = request.CheckC2SConnectionConstraint(); err != nil { + return newError("C2S connection constraint violation").Base(err) + } + initialPayload := buf.NewWithSize(65535) + encodedReader, err := request.CreateClientS2CReader(conn, initialPayload) + if err != nil { + return newError("failed to create client S2C reader").Base(err) + } + err = link.Writer.WriteMultiBuffer(buf.MultiBuffer{initialPayload}) + if err != nil { + return newError("failed to write initial payload").Base(err) + } + return buf.Copy(encodedReader, link.Writer, buf.UpdateActivity(timer)) + } + responseDoneAndCloseWriter := task.OnSuccess(responseDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDone, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + return nil + } else { + return newError("not implemented") + } +} + +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + return &Client{ + config: config, + ctx: ctx, + }, nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + clientConfig, ok := config.(*ClientConfig) + if !ok { + return nil, newError("not a ClientConfig") + } + return NewClient(ctx, clientConfig) + })) +} diff --git a/proxy/shadowsocks2022/config.pb.go b/proxy/shadowsocks2022/config.pb.go new file mode 100644 index 000000000..fbf35b501 --- /dev/null +++ b/proxy/shadowsocks2022/config.pb.go @@ -0,0 +1,195 @@ +package shadowsocks2022 + +import ( + net "github.com/v2fly/v2ray-core/v5/common/net" + _ "github.com/v2fly/v2ray-core/v5/common/protoext" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +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 ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Psk []byte `protobuf:"bytes,2,opt,name=psk,proto3" json:"psk,omitempty"` + Ipsk [][]byte `protobuf:"bytes,4,rep,name=ipsk,proto3" json:"ipsk,omitempty"` + Address *net.IPOrDomain `protobuf:"bytes,5,opt,name=address,proto3" json:"address,omitempty"` + Port uint32 `protobuf:"varint,6,opt,name=port,proto3" json:"port,omitempty"` +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_shadowsocks2022_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ClientConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientConfig) ProtoMessage() {} + +func (x *ClientConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_shadowsocks2022_config_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientConfig.ProtoReflect.Descriptor instead. +func (*ClientConfig) Descriptor() ([]byte, []int) { + return file_proxy_shadowsocks2022_config_proto_rawDescGZIP(), []int{0} +} + +func (x *ClientConfig) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *ClientConfig) GetPsk() []byte { + if x != nil { + return x.Psk + } + return nil +} + +func (x *ClientConfig) GetIpsk() [][]byte { + if x != nil { + return x.Ipsk + } + return nil +} + +func (x *ClientConfig) GetAddress() *net.IPOrDomain { + if x != nil { + return x.Address + } + return nil +} + +func (x *ClientConfig) GetPort() uint32 { + if x != nil { + return x.Port + } + return 0 +} + +var File_proxy_shadowsocks2022_config_proto protoreflect.FileDescriptor + +var file_proxy_shadowsocks2022_config_proto_rawDesc = []byte{ + 0x0a, 0x22, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, + 0x63, 0x6b, 0x73, 0x32, 0x30, 0x32, 0x32, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x20, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, 0x63, + 0x6b, 0x73, 0x32, 0x30, 0x32, 0x32, 0x1a, 0x18, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x6e, + 0x65, 0x74, 0x2f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x20, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x65, 0x78, + 0x74, 0x2f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xbe, 0x01, 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x70, + 0x73, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x03, 0x70, 0x73, 0x6b, 0x12, 0x12, 0x0a, + 0x04, 0x69, 0x70, 0x73, 0x6b, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x69, 0x70, 0x73, + 0x6b, 0x12, 0x3b, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x49, 0x50, 0x4f, 0x72, 0x44, + 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70, 0x6f, + 0x72, 0x74, 0x3a, 0x1f, 0x82, 0xb5, 0x18, 0x1b, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x12, 0x0f, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0x32, + 0x30, 0x32, 0x32, 0x42, 0x81, 0x01, 0x0a, 0x24, 0x63, 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x73, 0x68, 0x61, + 0x64, 0x6f, 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, 0x32, 0x30, 0x32, 0x32, 0x50, 0x01, 0x5a, 0x34, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x76, 0x32, 0x66, 0x6c, 0x79, + 0x2f, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x35, 0x2f, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x73, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, 0x63, 0x6b, 0x73, + 0x32, 0x30, 0x32, 0x32, 0xaa, 0x02, 0x20, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, + 0x65, 0x2e, 0x50, 0x72, 0x6f, 0x78, 0x79, 0x2e, 0x53, 0x68, 0x61, 0x64, 0x6f, 0x77, 0x73, 0x6f, + 0x63, 0x6b, 0x73, 0x32, 0x30, 0x32, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_shadowsocks2022_config_proto_rawDescOnce sync.Once + file_proxy_shadowsocks2022_config_proto_rawDescData = file_proxy_shadowsocks2022_config_proto_rawDesc +) + +func file_proxy_shadowsocks2022_config_proto_rawDescGZIP() []byte { + file_proxy_shadowsocks2022_config_proto_rawDescOnce.Do(func() { + file_proxy_shadowsocks2022_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_shadowsocks2022_config_proto_rawDescData) + }) + return file_proxy_shadowsocks2022_config_proto_rawDescData +} + +var file_proxy_shadowsocks2022_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_proxy_shadowsocks2022_config_proto_goTypes = []interface{}{ + (*ClientConfig)(nil), // 0: v2ray.core.proxy.shadowsocks2022.ClientConfig + (*net.IPOrDomain)(nil), // 1: v2ray.core.common.net.IPOrDomain +} +var file_proxy_shadowsocks2022_config_proto_depIdxs = []int32{ + 1, // 0: v2ray.core.proxy.shadowsocks2022.ClientConfig.address:type_name -> v2ray.core.common.net.IPOrDomain + 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_proxy_shadowsocks2022_config_proto_init() } +func file_proxy_shadowsocks2022_config_proto_init() { + if File_proxy_shadowsocks2022_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_shadowsocks2022_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ClientConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_proxy_shadowsocks2022_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_shadowsocks2022_config_proto_goTypes, + DependencyIndexes: file_proxy_shadowsocks2022_config_proto_depIdxs, + MessageInfos: file_proxy_shadowsocks2022_config_proto_msgTypes, + }.Build() + File_proxy_shadowsocks2022_config_proto = out.File + file_proxy_shadowsocks2022_config_proto_rawDesc = nil + file_proxy_shadowsocks2022_config_proto_goTypes = nil + file_proxy_shadowsocks2022_config_proto_depIdxs = nil +} diff --git a/proxy/shadowsocks2022/config.proto b/proxy/shadowsocks2022/config.proto new file mode 100644 index 000000000..c8aebb4d6 --- /dev/null +++ b/proxy/shadowsocks2022/config.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package v2ray.core.proxy.shadowsocks2022; +option csharp_namespace = "V2Ray.Core.Proxy.Shadowsocks2022"; +option go_package = "github.com/v2fly/v2ray-core/v5/proxy/shadowsocks2022"; +option java_package = "com.v2ray.core.proxy.shadowsocks2022"; +option java_multiple_files = true; + +import "common/net/address.proto"; +import "common/protoext/extensions.proto"; + +message ClientConfig { + option (v2ray.core.common.protoext.message_opt).type = "outbound"; + option (v2ray.core.common.protoext.message_opt).short_name = "shadowsocks2022"; + + string method = 1; + bytes psk = 2; + repeated bytes ipsk = 4; + + v2ray.core.common.net.IPOrDomain address = 5; + uint32 port = 6; +} diff --git a/proxy/shadowsocks2022/eih_aes.go b/proxy/shadowsocks2022/eih_aes.go new file mode 100644 index 000000000..eca9b053e --- /dev/null +++ b/proxy/shadowsocks2022/eih_aes.go @@ -0,0 +1,104 @@ +package shadowsocks2022 + +import ( + "github.com/lunixbochs/struc" + "github.com/v2fly/v2ray-core/v5/common/buf" + "io" + "lukechampine.com/blake3" +) + +func newAESEIH(size int) *aesEIH { + return &aesEIH{length: size} +} + +func newAESEIHWithData(size int, eih [][aesEIHSize]byte) *aesEIH { + return &aesEIH{length: size, eih: eih} +} + +const aesEIHSize = 16 + +type aesEIH struct { + eih [][aesEIHSize]byte + length int +} + +func (a *aesEIH) Pack(p []byte, opt *struc.Options) (int, error) { + var totalCopy int + for i := 0; i < a.length; i++ { + n := copy(p[aesEIHSize*i:aesEIHSize*(i+1)], a.eih[i][:]) + if n != 16 { + return 0, newError("failed to pack aesEIH") + } + totalCopy += n + } + return totalCopy, nil +} + +func (a *aesEIH) Unpack(r io.Reader, length int, opt *struc.Options) error { + a.eih = make([][aesEIHSize]byte, a.length) + for i := 0; i < a.length; i++ { + n, err := r.Read(a.eih[i][:]) + if err != nil { + return newError("failed to unpack aesEIH").Base(err) + } + if n != aesEIHSize { + return newError("failed to unpack aesEIH") + } + } + return nil +} + +func (a *aesEIH) Size(opt *struc.Options) int { + return a.length * aesEIHSize +} + +func (a *aesEIH) String() string { + return "" +} + +const aesEIHPskHashSize = 16 + +type aesEIHGenerator struct { + ipsk [][]byte + ipskHash [][aesEIHPskHashSize]byte + psk []byte + pskHash [aesEIHPskHashSize]byte + length int +} + +func newAESEIHGeneratorContainer(size int, effectivePsk []byte, ipsk [][]byte) *aesEIHGenerator { + var ipskHash [][aesEIHPskHashSize]byte + for _, v := range ipsk { + hash := blake3.Sum512(v) + ipskHash = append(ipskHash, [aesEIHPskHashSize]byte(hash[:16])) + } + pskHashFull := blake3.Sum512(effectivePsk) + pskHash := [aesEIHPskHashSize]byte(pskHashFull[:16]) + return &aesEIHGenerator{length: size, ipsk: ipsk, ipskHash: ipskHash, psk: effectivePsk, pskHash: pskHash} +} + +func (a *aesEIHGenerator) GenerateEIH(derivation KeyDerivation, method Method, salt []byte) (ExtensibleIdentityHeaders, error) { + eih := make([][16]byte, a.length) + current := a.length - 1 + currentPskHash := a.pskHash + for { + identityKeyBuf := buf.New() + identityKey := identityKeyBuf.Extend(int32(method.GetSessionSubKeyAndSaltLength())) + err := derivation.GetIdentitySubKey(a.ipsk[current], salt, identityKey) + if err != nil { + return nil, newError("failed to get identity sub key").Base(err) + } + eih[current] = [16]byte{} + err = method.GenerateEIH(identityKey, currentPskHash[:], eih[current][:]) + if err != nil { + return nil, newError("failed to generate EIH").Base(err) + } + current-- + if current < 0 { + break + } + currentPskHash = a.ipskHash[current] + identityKeyBuf.Release() + } + return newAESEIHWithData(a.length, eih), nil +} diff --git a/proxy/shadowsocks2022/encoding.go b/proxy/shadowsocks2022/encoding.go new file mode 100644 index 000000000..9d30b08f5 --- /dev/null +++ b/proxy/shadowsocks2022/encoding.go @@ -0,0 +1,255 @@ +package shadowsocks2022 + +import ( + "bytes" + "crypto/cipher" + cryptoRand "crypto/rand" + "io" + "math/rand" + "time" + + "github.com/lunixbochs/struc" + "github.com/v2fly/v2ray-core/v5/common/buf" + "github.com/v2fly/v2ray-core/v5/common/crypto" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/protocol" +) + +type TCPRequest struct { + keyDerivation KeyDerivation + method Method + + c2sSalt RequestSalt + c2sNonce crypto.BytesGenerator + c2sAEAD cipher.AEAD + + s2cSalt RequestSalt + s2cNonce crypto.BytesGenerator + s2cAEAD cipher.AEAD + + s2cSaltAssert RequestSalt + s2cInitialPayloadSize int +} + +func (t *TCPRequest) EncodeTCPRequestHeader(effectivePsk []byte, + eih [][]byte, address DestinationAddress, destPort int, initialPayload []byte, Out *buf.Buffer) error { + requestSalt := newRequestSaltWithLength(t.method.GetSessionSubKeyAndSaltLength()) + { + err := requestSalt.FillAllFrom(cryptoRand.Reader) + if err != nil { + return newError("failed to fill salt").Base(err) + } + } + t.c2sSalt = requestSalt + var sessionKey = make([]byte, t.method.GetSessionSubKeyAndSaltLength()) + { + err := t.keyDerivation.GetSessionSubKey(effectivePsk, requestSalt.Bytes(), sessionKey) + if err != nil { + return newError("failed to get session sub key").Base(err) + } + } + + aead, err := t.method.GetStreamAEAD(sessionKey) + if err != nil { + return newError("failed to get stream AEAD").Base(err) + } + t.c2sAEAD = aead + var paddingLength = TCPMinPaddingLength + if initialPayload == nil { + initialPayload = []byte{} + paddingLength += rand.Intn(TCPMaxPaddingLength) // TODO INSECURE RANDOM USED + } + + variableLengthHeader := &TCPRequestHeader3VariableLength{ + DestinationAddress: address, + Contents: struct { + PaddingLength uint16 `struc:"sizeof=Padding"` + Padding []byte + }(struct { + PaddingLength uint16 + Padding []byte + }{ + PaddingLength: uint16(paddingLength), + Padding: make([]byte, paddingLength), + }), + } + variableLengthHeaderBuffer := buf.New() + defer variableLengthHeaderBuffer.Release() + { + err := addrParser.WriteAddressPort(variableLengthHeaderBuffer, address, net.Port(destPort)) + if err != nil { + return newError("failed to write address port").Base(err) + } + } + { + err := struc.Pack(variableLengthHeaderBuffer, &variableLengthHeader.Contents) + if err != nil { + return newError("failed to pack variable length header").Base(err) + } + } + { + _, err := variableLengthHeaderBuffer.Write(initialPayload) + if err != nil { + return newError("failed to write initial payload").Base(err) + } + } + + fixedLengthHeader := &TCPRequestHeader2FixedLength{ + Type: TCPHeaderTypeClientToServerStream, + Timestamp: uint64(time.Now().Unix()), + HeaderLength: uint16(variableLengthHeaderBuffer.Len()), + } + + fixedLengthHeaderBuffer := buf.New() + defer fixedLengthHeaderBuffer.Release() + { + err := struc.Pack(fixedLengthHeaderBuffer, fixedLengthHeader) + if err != nil { + return newError("failed to pack fixed length header").Base(err) + } + } + eihGenerator := newAESEIHGeneratorContainer(len(eih), effectivePsk, eih) + eihHeader, err := eihGenerator.GenerateEIH(t.keyDerivation, t.method, requestSalt.Bytes()) + if err != nil { + return newError("failed to construct EIH").Base(err) + } + preSessionKeyHeader := &TCPRequestHeader1PreSessionKey{ + Salt: requestSalt, + EIH: eihHeader, + } + preSessionKeyHeaderBuffer := buf.New() + defer preSessionKeyHeaderBuffer.Release() + { + err := struc.Pack(preSessionKeyHeaderBuffer, preSessionKeyHeader) + if err != nil { + return newError("failed to pack pre session key header").Base(err) + } + } + requestNonce := crypto.GenerateInitialAEADNonce() + t.c2sNonce = requestNonce + { + n, err := Out.Write(preSessionKeyHeaderBuffer.BytesFrom(0)) + if err != nil { + return newError("failed to write pre session key header").Base(err) + } + if int32(n) != preSessionKeyHeaderBuffer.Len() { + return newError("failed to write pre session key header") + } + } + { + fixedLengthEncrypted := Out.Extend(fixedLengthHeaderBuffer.Len() + int32(aead.Overhead())) + aead.Seal(fixedLengthEncrypted[:0], requestNonce(), fixedLengthHeaderBuffer.Bytes(), nil) + } + { + variableLengthEncrypted := Out.Extend(variableLengthHeaderBuffer.Len() + int32(aead.Overhead())) + aead.Seal(variableLengthEncrypted[:0], requestNonce(), variableLengthHeaderBuffer.Bytes(), nil) + } + return nil +} + +func (t *TCPRequest) DecodeTCPResponseHeader(effectivePsk []byte, In io.Reader) error { + var preSessionKeyHeader TCPResponseHeader1PreSessionKey + preSessionKeyHeader.Salt = newRequestSaltWithLength(t.method.GetSessionSubKeyAndSaltLength()) + { + err := struc.Unpack(In, &preSessionKeyHeader) + if err != nil { + return newError("failed to unpack pre session key header").Base(err) + } + } + var s2cSalt = preSessionKeyHeader.Salt.Bytes() + t.s2cSalt = preSessionKeyHeader.Salt + var sessionKey = make([]byte, t.method.GetSessionSubKeyAndSaltLength()) + { + err := t.keyDerivation.GetSessionSubKey(effectivePsk, s2cSalt, sessionKey) + if err != nil { + return newError("failed to get session sub key").Base(err) + } + } + aead, err := t.method.GetStreamAEAD(sessionKey) + if err != nil { + return newError("failed to get stream AEAD").Base(err) + } + t.s2cAEAD = aead + + var fixedLengthHeaderEncryptedBuffer = buf.New() + defer fixedLengthHeaderEncryptedBuffer.Release() + { + _, err := fixedLengthHeaderEncryptedBuffer.ReadFullFrom(In, 11+int32(t.method.GetSessionSubKeyAndSaltLength())+int32(aead.Overhead())) + if err != nil { + return newError("failed to read fixed length header encrypted").Base(err) + } + } + s2cNonce := crypto.GenerateInitialAEADNonce() + t.s2cNonce = s2cNonce + var fixedLengthHeaderDecryptedBuffer = buf.New() + defer fixedLengthHeaderDecryptedBuffer.Release() + { + decryptionBuffer := fixedLengthHeaderDecryptedBuffer.Extend(11 + int32(t.method.GetSessionSubKeyAndSaltLength())) + _, err = aead.Open(decryptionBuffer[:0], s2cNonce(), fixedLengthHeaderEncryptedBuffer.Bytes(), nil) + if err != nil { + return newError("failed to decrypt fixed length header").Base(err) + } + } + var fixedLengthHeader TCPResponseHeader2FixedLength + fixedLengthHeader.RequestSalt = newRequestSaltWithLength(t.method.GetSessionSubKeyAndSaltLength()) + { + err := struc.Unpack(bytes.NewReader(fixedLengthHeaderDecryptedBuffer.Bytes()), &fixedLengthHeader) + if err != nil { + return newError("failed to unpack fixed length header").Base(err) + } + } + + if fixedLengthHeader.Type != TCPHeaderTypeServerToClientStream { + return newError("unexpected TCP header type") + } + timeDifference := int64(fixedLengthHeader.Timestamp) - time.Now().Unix() + if timeDifference < -60 || timeDifference > 60 { + return newError("timestamp is too far away") + } + + t.s2cSaltAssert = fixedLengthHeader.RequestSalt + t.s2cInitialPayloadSize = int(fixedLengthHeader.InitialPayloadLength) + return nil +} + +func (t *TCPRequest) CheckC2SConnectionConstraint() error { + if bytes.Compare(t.c2sSalt.Bytes(), t.s2cSaltAssert.Bytes()) != 0 { + return newError("c2s salt not equal to s2c salt assert") + } + return nil +} + +func (t *TCPRequest) CreateClientS2CReader(In io.Reader, initialPayload *buf.Buffer) (buf.Reader, error) { + AEADAuthenticator := &crypto.AEADAuthenticator{ + AEAD: t.s2cAEAD, + NonceGenerator: t.s2cNonce, + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + initialPayloadEncrypted := buf.NewWithSize(65535) + defer initialPayloadEncrypted.Release() + initialPayloadEncryptedBytes := initialPayloadEncrypted.Extend(int32(t.s2cAEAD.Overhead()) + int32(t.s2cInitialPayloadSize)) + _, err := io.ReadFull(In, initialPayloadEncryptedBytes) + if err != nil { + return nil, newError("failed to read initial payload").Base(err) + } + initialPayloadBytes := initialPayload.Extend(int32(t.s2cInitialPayloadSize)) + _, err = t.s2cAEAD.Open(initialPayloadBytes[:0], t.s2cNonce(), initialPayloadEncryptedBytes, nil) + if err != nil { + return nil, newError("failed to decrypt initial payload").Base(err) + } + return crypto.NewAuthenticationReader(AEADAuthenticator, &crypto.AEADChunkSizeParser{ + Auth: AEADAuthenticator, + }, In, protocol.TransferTypeStream, nil), nil +} + +func (t *TCPRequest) CreateClientC2SWriter(writer io.Writer) buf.Writer { + AEADAuthenticator := &crypto.AEADAuthenticator{ + AEAD: t.c2sAEAD, + NonceGenerator: t.c2sNonce, + AdditionalDataGenerator: crypto.GenerateEmptyBytes(), + } + sizeParser := &crypto.AEADChunkSizeParser{ + Auth: AEADAuthenticator, + } + return crypto.NewAuthenticationWriter(AEADAuthenticator, sizeParser, writer, protocol.TransferTypeStream, nil) +} diff --git a/proxy/shadowsocks2022/errors.generated.go b/proxy/shadowsocks2022/errors.generated.go new file mode 100644 index 000000000..98f475bb3 --- /dev/null +++ b/proxy/shadowsocks2022/errors.generated.go @@ -0,0 +1,9 @@ +package shadowsocks2022 + +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/proxy/shadowsocks2022/kdf_blake3.go b/proxy/shadowsocks2022/kdf_blake3.go new file mode 100644 index 000000000..9a6ca813d --- /dev/null +++ b/proxy/shadowsocks2022/kdf_blake3.go @@ -0,0 +1,31 @@ +package shadowsocks2022 + +import ( + "github.com/v2fly/v2ray-core/v5/common/buf" + "lukechampine.com/blake3" +) + +func newBLAKE3KeyDerivation() *BLAKE3KeyDerivation { + return &BLAKE3KeyDerivation{} +} + +type BLAKE3KeyDerivation struct { +} + +func (B BLAKE3KeyDerivation) GetSessionSubKey(effectivePsk, Salt []byte, OutKey []byte) error { + keyingMaterialBuffer := buf.New() + keyingMaterialBuffer.Write(effectivePsk) + keyingMaterialBuffer.Write(Salt) + blake3.DeriveKey(OutKey, "shadowsocks 2022 session subkey", keyingMaterialBuffer.Bytes()) + keyingMaterialBuffer.Release() + return nil +} + +func (B BLAKE3KeyDerivation) GetIdentitySubKey(effectivePsk, Salt []byte, OutKey []byte) error { + keyingMaterialBuffer := buf.New() + keyingMaterialBuffer.Write(effectivePsk) + keyingMaterialBuffer.Write(Salt) + blake3.DeriveKey(OutKey, "shadowsocks 2022 identity subkey", keyingMaterialBuffer.Bytes()) + keyingMaterialBuffer.Release() + return nil +} diff --git a/proxy/shadowsocks2022/method_aes128gcm.go b/proxy/shadowsocks2022/method_aes128gcm.go new file mode 100644 index 000000000..50d61abb1 --- /dev/null +++ b/proxy/shadowsocks2022/method_aes128gcm.go @@ -0,0 +1,38 @@ +package shadowsocks2022 + +import ( + "crypto/aes" + "crypto/cipher" +) + +func newAES128GCMMethod() *AES128GCMMethod { + return &AES128GCMMethod{} +} + +type AES128GCMMethod struct { +} + +func (A AES128GCMMethod) GetSessionSubKeyAndSaltLength() int { + return 16 +} + +func (A AES128GCMMethod) GetStreamAEAD(SessionSubKey []byte) (cipher.AEAD, error) { + aesCipher, err := aes.NewCipher(SessionSubKey) + if err != nil { + return nil, newError("failed to create AES cipher").Base(err) + } + aead, err := cipher.NewGCM(aesCipher) + if err != nil { + return nil, newError("failed to create AES-GCM AEAD").Base(err) + } + return aead, nil +} + +func (A AES128GCMMethod) GenerateEIH(CurrentIdentitySubKey []byte, nextPskHash []byte, out []byte) error { + aesCipher, err := aes.NewCipher(CurrentIdentitySubKey) + if err != nil { + return newError("failed to create AES cipher").Base(err) + } + aesCipher.Encrypt(out, nextPskHash) + return nil +} diff --git a/proxy/shadowsocks2022/method_aes256gcm.go b/proxy/shadowsocks2022/method_aes256gcm.go new file mode 100644 index 000000000..52f7c30b1 --- /dev/null +++ b/proxy/shadowsocks2022/method_aes256gcm.go @@ -0,0 +1,38 @@ +package shadowsocks2022 + +import ( + "crypto/aes" + "crypto/cipher" +) + +func newAES256GCMMethod() *AES256GCMMethod { + return &AES256GCMMethod{} +} + +type AES256GCMMethod struct { +} + +func (A AES256GCMMethod) GetSessionSubKeyAndSaltLength() int { + return 32 +} + +func (A AES256GCMMethod) GetStreamAEAD(SessionSubKey []byte) (cipher.AEAD, error) { + aesCipher, err := aes.NewCipher(SessionSubKey) + if err != nil { + return nil, newError("failed to create AES cipher").Base(err) + } + aead, err := cipher.NewGCM(aesCipher) + if err != nil { + return nil, newError("failed to create AES-GCM AEAD").Base(err) + } + return aead, nil +} + +func (A AES256GCMMethod) GenerateEIH(CurrentIdentitySubKey []byte, nextPskHash []byte, out []byte) error { + aesCipher, err := aes.NewCipher(CurrentIdentitySubKey) + if err != nil { + return newError("failed to create AES cipher").Base(err) + } + aesCipher.Encrypt(out, nextPskHash) + return nil +} diff --git a/proxy/shadowsocks2022/requestsalt.go b/proxy/shadowsocks2022/requestsalt.go new file mode 100644 index 000000000..8f66a74e6 --- /dev/null +++ b/proxy/shadowsocks2022/requestsalt.go @@ -0,0 +1,58 @@ +package shadowsocks2022 + +import ( + "encoding/hex" + "github.com/lunixbochs/struc" + "io" +) + +func newRequestSaltWithLength(length int) RequestSalt { + return &requestSaltWithLength{length: length} +} + +type requestSaltWithLength struct { + length int + content []byte +} + +func (r *requestSaltWithLength) isRequestSalt() {} +func (r *requestSaltWithLength) Pack(p []byte, opt *struc.Options) (int, error) { + n := copy(p, r.content) + if n != r.length { + return 0, newError("failed to pack request salt with length") + } + return n, nil +} + +func (r *requestSaltWithLength) Unpack(reader io.Reader, length int, opt *struc.Options) error { + r.content = make([]byte, r.length) + n, err := io.ReadFull(reader, r.content) + if err != nil { + return newError("failed to unpack request salt with length").Base(err) + } + if n != r.length { + return newError("failed to unpack request salt with length") + } + return nil +} + +func (r *requestSaltWithLength) Size(opt *struc.Options) int { + return r.length +} + +func (r *requestSaltWithLength) String() string { + return hex.Dump(r.content) +} + +func (r *requestSaltWithLength) Bytes() []byte { + return r.content +} + +func (r *requestSaltWithLength) FillAllFrom(reader io.Reader) error { + r.content = make([]byte, r.length) + _, err := io.ReadFull(reader, r.content) + if err != nil { + return newError("failed to fill salt from reader").Base(err) + } + return nil +} diff --git a/proxy/shadowsocks2022/ss2022.go b/proxy/shadowsocks2022/ss2022.go new file mode 100644 index 000000000..6d5bfac54 --- /dev/null +++ b/proxy/shadowsocks2022/ss2022.go @@ -0,0 +1,88 @@ +package shadowsocks2022 + +import ( + "crypto/cipher" + "github.com/lunixbochs/struc" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/protocol" + "io" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +type KeyDerivation interface { + GetSessionSubKey(effectivePsk, Salt []byte, OutKey []byte) error + GetIdentitySubKey(effectivePsk, Salt []byte, OutKey []byte) error +} + +type Method interface { + GetSessionSubKeyAndSaltLength() int + GetStreamAEAD(SessionSubKey []byte) (cipher.AEAD, error) + GenerateEIH(CurrentIdentitySubKey []byte, nextPskHash []byte, out []byte) error +} + +type ExtensibleIdentityHeaders interface { + struc.Custom +} + +type DestinationAddress interface { + net.Address +} + +type RequestSalt interface { + struc.Custom + isRequestSalt() + Bytes() []byte + FillAllFrom(reader io.Reader) error +} + +type TCPRequestHeader1PreSessionKey struct { + Salt RequestSalt + EIH ExtensibleIdentityHeaders +} + +type TCPRequestHeader2FixedLength struct { + Type byte + Timestamp uint64 + HeaderLength uint16 +} + +type TCPRequestHeader3VariableLength struct { + DestinationAddress DestinationAddress + Contents struct { + PaddingLength uint16 `struc:"sizeof=Padding"` + Padding []byte + } +} + +type TCPRequestHeader struct { + PreSessionKeyHeader TCPRequestHeader1PreSessionKey + FixedLengthHeader TCPRequestHeader2FixedLength + Header TCPRequestHeader3VariableLength +} + +type TCPResponseHeader1PreSessionKey struct { + Salt RequestSalt +} + +type TCPResponseHeader2FixedLength struct { + Type byte + Timestamp uint64 + RequestSalt RequestSalt + InitialPayloadLength uint16 +} +type TCPResponseHeader struct { + PreSessionKeyHeader TCPResponseHeader1PreSessionKey + Header TCPResponseHeader2FixedLength +} + +const TCPHeaderTypeClientToServerStream = byte(0x00) +const TCPHeaderTypeServerToClientStream = byte(0x01) +const TCPMinPaddingLength = 0 +const TCPMaxPaddingLength = 900 + +var addrParser = protocol.NewAddressParser( + protocol.AddressFamilyByte(0x01, net.AddressFamilyIPv4), + protocol.AddressFamilyByte(0x04, net.AddressFamilyIPv6), + protocol.AddressFamilyByte(0x03, net.AddressFamilyDomain), +)