diff --git a/adapter/outbound/vless.go b/adapter/outbound/vless.go new file mode 100644 index 00000000..0298dd88 --- /dev/null +++ b/adapter/outbound/vless.go @@ -0,0 +1,369 @@ +package outbound + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "strconv" + + "github.com/Dreamacro/clash/component/dialer" + "github.com/Dreamacro/clash/component/resolver" + C "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/transport/gun" + "github.com/Dreamacro/clash/transport/vless" + "github.com/Dreamacro/clash/transport/vmess" + "golang.org/x/net/http2" +) + +type Vless struct { + *Base + client *vless.Client + option *VlessOption + + // for gun mux + gunTLSConfig *tls.Config + gunConfig *gun.Config + transport *http2.Transport +} + +type VlessOption struct { + BasicOption + Name string `proxy:"name"` + Server string `proxy:"server"` + Port int `proxy:"port"` + UUID string `proxy:"uuid"` + Flow string `proxy:"flow,omitempty"` + FlowShow bool `proxy:"flow-show,omitempty"` + TLS bool `proxy:"tls,omitempty"` + UDP bool `proxy:"udp,omitempty"` + Network string `proxy:"network,omitempty"` + HTTPOpts HTTPOptions `proxy:"http-opts,omitempty"` + HTTP2Opts HTTP2Options `proxy:"h2-opts,omitempty"` + GrpcOpts GrpcOptions `proxy:"grpc-opts,omitempty"` + WSOpts WSOptions `proxy:"ws-opts,omitempty"` + WSPath string `proxy:"ws-path,omitempty"` + WSHeaders map[string]string `proxy:"ws-headers,omitempty"` + SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"` + ServerName string `proxy:"servername,omitempty"` +} + +func (v *Vless) StreamConn(c net.Conn, metadata *C.Metadata) (net.Conn, error) { + var err error + switch v.option.Network { + case "ws": + if v.option.WSOpts.Path == "" { + v.option.WSOpts.Path = v.option.WSPath + } + if len(v.option.WSOpts.Headers) == 0 { + v.option.WSOpts.Headers = v.option.WSHeaders + } + + host, port, _ := net.SplitHostPort(v.addr) + wsOpts := &vmess.WebsocketConfig{ + Host: host, + Port: port, + Path: v.option.WSOpts.Path, + MaxEarlyData: v.option.WSOpts.MaxEarlyData, + EarlyDataHeaderName: v.option.WSOpts.EarlyDataHeaderName, + } + + if len(v.option.WSOpts.Headers) != 0 { + header := http.Header{} + for key, value := range v.option.WSOpts.Headers { + header.Add(key, value) + } + wsOpts.Headers = header + } + + if v.option.TLS { + wsOpts.TLS = true + wsOpts.TLSConfig = &tls.Config{ + ServerName: host, + InsecureSkipVerify: v.option.SkipCertVerify, + NextProtos: []string{"http/1.1"}, + } + if v.option.ServerName != "" { + wsOpts.TLSConfig.ServerName = v.option.ServerName + } else if host := wsOpts.Headers.Get("Host"); host != "" { + wsOpts.TLSConfig.ServerName = host + } + } + c, err = vmess.StreamWebsocketConn(c, wsOpts) + case "http": + // readability first, so just copy default TLS logic + c, err = v.streamTLSOrXTLSConn(c, false) + if err != nil { + return nil, err + } + + host, _, _ := net.SplitHostPort(v.addr) + httpOpts := &vmess.HTTPConfig{ + Host: host, + Method: v.option.HTTPOpts.Method, + Path: v.option.HTTPOpts.Path, + Headers: v.option.HTTPOpts.Headers, + } + + c = vmess.StreamHTTPConn(c, httpOpts) + case "h2": + c, err = v.streamTLSOrXTLSConn(c, true) + if err != nil { + return nil, err + } + + h2Opts := &vmess.H2Config{ + Hosts: v.option.HTTP2Opts.Host, + Path: v.option.HTTP2Opts.Path, + } + + c, err = vmess.StreamH2Conn(c, h2Opts) + case "grpc": + if v.isXTLSEnabled() { + c, err = gun.StreamGunWithXTLSConn(c, v.gunTLSConfig, v.gunConfig) + } else { + c, err = gun.StreamGunWithConn(c, v.gunTLSConfig, v.gunConfig) + } + default: + // handle TLS And XTLS + c, err = v.streamTLSOrXTLSConn(c, true) + } + + if err != nil { + return nil, err + } + + return v.client.StreamConn(c, parseVlessAddr(metadata)) +} + +func (v *Vless) streamTLSOrXTLSConn(conn net.Conn, isH2 bool) (net.Conn, error) { + host, _, _ := net.SplitHostPort(v.addr) + + if v.isXTLSEnabled() { + xtlsOpts := vless.XTLSConfig{ + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + } + + if isH2 { + xtlsOpts.NextProtos = []string{"h2"} + } + + if v.option.ServerName != "" { + xtlsOpts.Host = v.option.ServerName + } + + return vless.StreamXTLSConn(conn, &xtlsOpts) + + } else if v.option.TLS { + tlsOpts := vmess.TLSConfig{ + Host: host, + SkipCertVerify: v.option.SkipCertVerify, + } + + if isH2 { + tlsOpts.NextProtos = []string{"h2"} + } + + if v.option.ServerName != "" { + tlsOpts.Host = v.option.ServerName + } + + return vmess.StreamTLSConn(conn, &tlsOpts) + } + + return conn, nil +} + +func (v *Vless) isXTLSEnabled() bool { + return v.client.Addons != nil +} + +// DialContext implements C.ProxyAdapter +func (v *Vless) DialContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.Conn, err error) { + // gun transport + if v.transport != nil && len(opts) == 0 { + c, err := gun.StreamGunWithTransport(v.transport, v.gunConfig) + if err != nil { + return nil, err + } + defer safeConnClose(c, err) + + c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) + if err != nil { + return nil, err + } + + return NewConn(c, v), nil + } + + c, err := dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + defer safeConnClose(c, err) + + c, err = v.StreamConn(c, metadata) + return NewConn(c, v), err +} + +// ListenPacketContext implements C.ProxyAdapter +func (v *Vless) ListenPacketContext(ctx context.Context, metadata *C.Metadata, opts ...dialer.Option) (_ C.PacketConn, err error) { + // vmess use stream-oriented udp with a special address, so we needs a net.UDPAddr + if !metadata.Resolved() { + ip, err := resolver.ResolveIP(metadata.Host) + if err != nil { + return nil, errors.New("can't resolve ip") + } + metadata.DstIP = ip + } + + var c net.Conn + // gun transport + if v.transport != nil && len(opts) == 0 { + c, err = gun.StreamGunWithTransport(v.transport, v.gunConfig) + if err != nil { + return nil, err + } + defer safeConnClose(c, err) + + c, err = v.client.StreamConn(c, parseVlessAddr(metadata)) + } else { + c, err = dialer.DialContext(ctx, "tcp", v.addr, v.Base.DialOptions(opts...)...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + defer safeConnClose(c, err) + + c, err = v.StreamConn(c, metadata) + } + + if err != nil { + return nil, fmt.Errorf("new vless client error: %v", err) + } + + return newPacketConn(&vlessPacketConn{Conn: c, rAddr: metadata.UDPAddr()}, v), nil +} + +func parseVlessAddr(metadata *C.Metadata) *vless.DstAddr { + var addrType byte + var addr []byte + switch metadata.AddrType { + case C.AtypIPv4: + addrType = byte(vless.AtypIPv4) + addr = make([]byte, net.IPv4len) + copy(addr[:], metadata.DstIP.To4()) + case C.AtypIPv6: + addrType = byte(vless.AtypIPv6) + addr = make([]byte, net.IPv6len) + copy(addr[:], metadata.DstIP.To16()) + case C.AtypDomainName: + addrType = byte(vless.AtypDomainName) + addr = make([]byte, len(metadata.Host)+1) + addr[0] = byte(len(metadata.Host)) + copy(addr[1:], []byte(metadata.Host)) + } + + port, _ := strconv.Atoi(metadata.DstPort) + return &vless.DstAddr{ + UDP: metadata.NetWork == C.UDP, + AddrType: addrType, + Addr: addr, + Port: uint(port), + } +} + +type vlessPacketConn struct { + net.Conn + rAddr net.Addr +} + +func (uc *vlessPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { + return uc.Conn.Write(b) +} + +func (uc *vlessPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { + n, err := uc.Conn.Read(b) + return n, uc.rAddr, err +} + +func NewVless(option VlessOption) (*Vless, error) { + if !option.TLS { + return nil, fmt.Errorf("TLS must be true with vless") + } + + var addons *vless.Addons + if option.Network != "ws" && len(option.Flow) >= 16 { + option.Flow = option.Flow[:16] + switch option.Flow { + case vless.XRO, vless.XRD, vless.XRS: + addons = &vless.Addons{ + Flow: option.Flow, + } + default: + return nil, fmt.Errorf("unsupported vless flow type: %s", option.Flow) + } + } + + client, err := vless.NewClient(option.UUID, addons, option.FlowShow) + if err != nil { + return nil, err + } + + v := &Vless{ + Base: &Base{ + name: option.Name, + addr: net.JoinHostPort(option.Server, strconv.Itoa(option.Port)), + tp: C.Vless, + udp: option.UDP, + iface: option.Interface, + }, + client: client, + option: &option, + } + + switch option.Network { + case "h2": + if len(option.HTTP2Opts.Host) == 0 { + option.HTTP2Opts.Host = append(option.HTTP2Opts.Host, "www.example.com") + } + case "grpc": + dialFn := func(network, addr string) (net.Conn, error) { + c, err := dialer.DialContext(context.Background(), "tcp", v.addr, v.Base.DialOptions()...) + if err != nil { + return nil, fmt.Errorf("%s connect error: %s", v.addr, err.Error()) + } + tcpKeepAlive(c) + return c, nil + } + + gunConfig := &gun.Config{ + ServiceName: v.option.GrpcOpts.GrpcServiceName, + Host: v.option.ServerName, + } + tlsConfig := &tls.Config{ + InsecureSkipVerify: v.option.SkipCertVerify, + ServerName: v.option.ServerName, + } + + if v.option.ServerName == "" { + host, _, _ := net.SplitHostPort(v.addr) + tlsConfig.ServerName = host + gunConfig.Host = host + } + + v.gunTLSConfig = tlsConfig + v.gunConfig = gunConfig + if v.isXTLSEnabled() { + v.transport = gun.NewHTTP2XTLSClient(dialFn, tlsConfig) + } else { + v.transport = gun.NewHTTP2Client(dialFn, tlsConfig) + } + } + + return v, nil +} diff --git a/transport/vless/config.pb.go b/transport/vless/config.pb.go new file mode 100644 index 00000000..b622def3 --- /dev/null +++ b/transport/vless/config.pb.go @@ -0,0 +1,158 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: transport/vless/config.proto + +package vless + +import ( + 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 Addons struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Flow string `protobuf:"bytes,1,opt,name=Flow,proto3" json:"Flow,omitempty"` + Seed []byte `protobuf:"bytes,2,opt,name=Seed,proto3" json:"Seed,omitempty"` +} + +func (x *Addons) Reset() { + *x = Addons{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_vless_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Addons) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Addons) ProtoMessage() {} + +func (x *Addons) ProtoReflect() protoreflect.Message { + mi := &file_transport_vless_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 Addons.ProtoReflect.Descriptor instead. +func (*Addons) Descriptor() ([]byte, []int) { + return file_transport_vless_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Addons) GetFlow() string { + if x != nil { + return x.Flow + } + return "" +} + +func (x *Addons) GetSeed() []byte { + if x != nil { + return x.Seed + } + return nil +} + +var File_transport_vless_config_proto protoreflect.FileDescriptor + +var file_transport_vless_config_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x6c, 0x65, 0x73, + 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x15, + 0x63, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, + 0x76, 0x6c, 0x65, 0x73, 0x73, 0x22, 0x30, 0x0a, 0x06, 0x41, 0x64, 0x64, 0x6f, 0x6e, 0x73, 0x12, + 0x12, 0x0a, 0x04, 0x46, 0x6c, 0x6f, 0x77, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x46, + 0x6c, 0x6f, 0x77, 0x12, 0x12, 0x0a, 0x04, 0x53, 0x65, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0c, 0x52, 0x04, 0x53, 0x65, 0x65, 0x64, 0x42, 0x61, 0x0a, 0x19, 0x63, 0x6f, 0x6d, 0x2e, 0x63, + 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x76, + 0x6c, 0x65, 0x73, 0x73, 0x50, 0x01, 0x5a, 0x2a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x44, 0x72, 0x65, 0x61, 0x6d, 0x61, 0x63, 0x72, 0x6f, 0x2f, 0x63, 0x6c, 0x61, + 0x73, 0x68, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x76, 0x6c, 0x65, + 0x73, 0x73, 0xaa, 0x02, 0x15, 0x43, 0x6c, 0x61, 0x73, 0x68, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x56, 0x6c, 0x65, 0x73, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_transport_vless_config_proto_rawDescOnce sync.Once + file_transport_vless_config_proto_rawDescData = file_transport_vless_config_proto_rawDesc +) + +func file_transport_vless_config_proto_rawDescGZIP() []byte { + file_transport_vless_config_proto_rawDescOnce.Do(func() { + file_transport_vless_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_vless_config_proto_rawDescData) + }) + return file_transport_vless_config_proto_rawDescData +} + +var file_transport_vless_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_transport_vless_config_proto_goTypes = []interface{}{ + (*Addons)(nil), // 0: clash.transport.vless.Addons +} +var file_transport_vless_config_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_transport_vless_config_proto_init() } +func file_transport_vless_config_proto_init() { + if File_transport_vless_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_vless_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Addons); 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_transport_vless_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_vless_config_proto_goTypes, + DependencyIndexes: file_transport_vless_config_proto_depIdxs, + MessageInfos: file_transport_vless_config_proto_msgTypes, + }.Build() + File_transport_vless_config_proto = out.File + file_transport_vless_config_proto_rawDesc = nil + file_transport_vless_config_proto_goTypes = nil + file_transport_vless_config_proto_depIdxs = nil +} diff --git a/transport/vless/config.proto b/transport/vless/config.proto new file mode 100644 index 00000000..80900230 --- /dev/null +++ b/transport/vless/config.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; + +package clash.transport.vless; +option csharp_namespace = "Clash.Transport.Vless"; +option go_package = "github.com/Dreamacro/clash/transport/vless"; +option java_package = "com.clash.transport.vless"; +option java_multiple_files = true; + +message Addons { + string Flow = 1; + bytes Seed = 2; +} \ No newline at end of file diff --git a/transport/vless/conn.go b/transport/vless/conn.go new file mode 100644 index 00000000..e6e6e34c --- /dev/null +++ b/transport/vless/conn.go @@ -0,0 +1,128 @@ +package vless + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + + "github.com/gofrs/uuid" + xtls "github.com/xtls/go" + "google.golang.org/protobuf/proto" +) + +type Conn struct { + net.Conn + dst *DstAddr + id *uuid.UUID + addons *Addons + received bool +} + +func (vc *Conn) Read(b []byte) (int, error) { + if vc.received { + return vc.Conn.Read(b) + } + + if err := vc.recvResponse(); err != nil { + return 0, err + } + vc.received = true + return vc.Conn.Read(b) +} + +func (vc *Conn) sendRequest() error { + buf := &bytes.Buffer{} + + buf.WriteByte(Version) // protocol version + buf.Write(vc.id.Bytes()) // 16 bytes of uuid + + if vc.addons != nil { + bytes, err := proto.Marshal(vc.addons) + if err != nil { + return err + } + + buf.WriteByte(byte(len(bytes))) + buf.Write(bytes) + } else { + buf.WriteByte(0) // addon data length. 0 means no addon data + } + + // command + if vc.dst.UDP { + buf.WriteByte(CommandUDP) + } else { + buf.WriteByte(CommandTCP) + } + + // Port AddrType Addr + binary.Write(buf, binary.BigEndian, uint16(vc.dst.Port)) + buf.WriteByte(vc.dst.AddrType) + buf.Write(vc.dst.Addr) + + _, err := vc.Conn.Write(buf.Bytes()) + return err +} + +func (vc *Conn) recvResponse() error { + var err error + buf := make([]byte, 1) + _, err = io.ReadFull(vc.Conn, buf) + if err != nil { + return err + } + + if buf[0] != Version { + return errors.New("unexpected response version") + } + + _, err = io.ReadFull(vc.Conn, buf) + if err != nil { + return err + } + + length := int64(buf[0]) + if length != 0 { // addon data length > 0 + io.CopyN(io.Discard, vc.Conn, length) // just discard + } + + return nil +} + +// newConn return a Conn instance +func newConn(conn net.Conn, client *Client, dst *DstAddr) (*Conn, error) { + c := &Conn{ + Conn: conn, + id: client.uuid, + dst: dst, + } + + if !dst.UDP && client.Addons != nil { + switch client.Addons.Flow { + case XRO, XRD, XRS: + if xtlsConn, ok := conn.(*xtls.Conn); ok { + xtlsConn.RPRX = true + xtlsConn.SHOW = client.XTLSShow + xtlsConn.MARK = "XTLS" + if client.Addons.Flow == XRS { + client.Addons.Flow = XRD + } + + if client.Addons.Flow == XRD { + xtlsConn.DirectMode = true + } + c.addons = client.Addons + } else { + return nil, fmt.Errorf("failed to use %s, maybe \"security\" is not \"xtls\"", client.Addons.Flow) + } + } + } + + if err := c.sendRequest(); err != nil { + return nil, err + } + return c, nil +} diff --git a/transport/vless/vless.go b/transport/vless/vless.go new file mode 100644 index 00000000..458f54de --- /dev/null +++ b/transport/vless/vless.go @@ -0,0 +1,71 @@ +package vless + +import ( + "net" + + "github.com/gofrs/uuid" +) + +const ( + XRO = "xtls-rprx-origin" + XRD = "xtls-rprx-direct" + XRS = "xtls-rprx-splice" + + Version byte = 0 // protocol version. preview version is 0 +) + +// Command types +const ( + CommandTCP byte = 1 + CommandUDP byte = 2 +) + +// Addr types +const ( + AtypIPv4 byte = 1 + AtypDomainName byte = 2 + AtypIPv6 byte = 3 +) + +// DstAddr store destination address +type DstAddr struct { + UDP bool + AddrType byte + Addr []byte + Port uint +} + +// Config of vless +type Config struct { + UUID string + AlterID uint16 + Security string + Port string + HostName string +} + +// Client is vless connection generator +type Client struct { + uuid *uuid.UUID + Addons *Addons + XTLSShow bool +} + +// StreamConn return a Conn with net.Conn and DstAddr +func (c *Client) StreamConn(conn net.Conn, dst *DstAddr) (net.Conn, error) { + return newConn(conn, c, dst) +} + +// NewClient return Client instance +func NewClient(uuidStr string, addons *Addons, xtlsShow bool) (*Client, error) { + uid, err := uuid.FromString(uuidStr) + if err != nil { + return nil, err + } + + return &Client{ + uuid: &uid, + Addons: addons, + XTLSShow: xtlsShow, + }, nil +} diff --git a/transport/vless/xtls.go b/transport/vless/xtls.go new file mode 100644 index 00000000..69035aa0 --- /dev/null +++ b/transport/vless/xtls.go @@ -0,0 +1,25 @@ +package vless + +import ( + "net" + + xtls "github.com/xtls/go" +) + +type XTLSConfig struct { + Host string + SkipCertVerify bool + NextProtos []string +} + +func StreamXTLSConn(conn net.Conn, cfg *XTLSConfig) (net.Conn, error) { + xtlsConfig := &xtls.Config{ + ServerName: cfg.Host, + InsecureSkipVerify: cfg.SkipCertVerify, + NextProtos: cfg.NextProtos, + } + + xtlsConn := xtls.Client(conn, xtlsConfig) + err := xtlsConn.Handshake() + return xtlsConn, err +}