Compare commits

..

15 Commits

Author SHA1 Message Date
yuhan6665
d218a5e8c5 Increase to 16 parallel streams 2025-04-19 23:50:18 -04:00
yuhan6665
3d7ed5450b Stream reader read twice for large muxcool packet 2025-04-19 23:14:22 -04:00
yuhan6665
c29af68a2f Glue mux meta with data in one frame for Datagram 2025-04-19 23:13:16 -04:00
yuhan6665
e4a5344189 Temp: add some logs 2025-04-06 17:27:24 -04:00
yuhan6665
33acf3c2b6 Add multi buffer reader 2025-03-16 23:07:56 -04:00
yuhan6665
7c78408df9 Add some logs for troubleshooting 2025-03-09 22:59:30 -04:00
yuhan6665
129b2be9c1 Use capsulate protocol for large UDP packet
- make datagram transport without mux functionality
- it is now recommended to always pair with mux-cool (XUDP new tunnel non-zero session id)
2025-03-09 09:48:43 -04:00
yuhan6665
358bdc258e Update to latest main 2025-03-09 09:48:43 -04:00
yuhan6665
c660bf7e37 Add Datagram transport 2025-03-09 09:48:43 -04:00
yuhan6665
414379ca96 Add config proto 2025-03-09 09:48:43 -04:00
RPRX
2cba2c4d59 v25.3.6
Announcement of NFTs by Project X: https://github.com/XTLS/Xray-core/discussions/3633
Project X NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/1

XHTTP: Beyond REALITY: https://github.com/XTLS/Xray-core/discussions/4113
REALITY NFT: https://opensea.io/assets/ethereum/0x5ee362866001613093361eb8569d59c4141b76d1/2
2025-03-06 13:50:15 +00:00
Happ-dev
306fa51475 README.md: Add Happ to iOS clients (#4465) 2025-03-06 12:27:25 +00:00
dependabot[bot]
6d6f1c6967 Bump golang.org/x/net from 0.36.0 to 0.37.0 (#4469)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.36.0 to 0.37.0.
- [Commits](https://github.com/golang/net/compare/v0.36.0...v0.37.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-06 10:59:49 +00:00
dependabot[bot]
152959824f Bump google.golang.org/grpc from 1.70.0 to 1.71.0 (#4463)
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.70.0 to 1.71.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.70.0...v1.71.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 09:23:38 +00:00
dependabot[bot]
a977b6357e Bump golang.org/x/net from 0.35.0 to 0.36.0 (#4462)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.35.0 to 0.36.0.
- [Commits](https://github.com/golang/net/compare/v0.35.0...v0.36.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-05 09:23:31 +00:00
28 changed files with 839 additions and 554 deletions

View File

@@ -85,6 +85,7 @@
- [X-flutter](https://github.com/XTLS/X-flutter)
- [SaeedDev94/Xray](https://github.com/SaeedDev94/Xray)
- iOS & macOS arm64
- [Happ](https://apps.apple.com/app/happ-proxy-utility/id6504287215)
- [FoXray](https://apps.apple.com/app/foxray/id6448898396)
- [Streisand](https://apps.apple.com/app/streisand/id6450534064)
- macOS arm64 & x64

View File

@@ -210,34 +210,6 @@ func (s *DNS) LookupIP(domain string, option dns.IPOption) ([]net.IP, error) {
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
}
func (s *DNS) LookupHTTPS(domain string) (map[string]string, error) {
errs := []error{}
ctx := session.ContextWithInbound(s.ctx, &session.Inbound{Tag: s.tag})
for _, client := range s.sortClients(domain) {
if strings.EqualFold(client.Name(), "FakeDNS") {
errors.LogDebug(s.ctx, "skip DNS resolution for domain ", domain, " at server ", client.Name())
continue
}
EnhancedServer, ok := client.server.(EnhancedServer)
if !ok {
continue
}
HTTPSRecord, err := EnhancedServer.QueryHTTPS(ctx, domain, s.disableCache)
if len(HTTPSRecord) > 0 {
return HTTPSRecord, nil
}
if err != nil {
errors.LogInfoInner(s.ctx, err, "failed to lookup HTTPS for domain ", domain, " at server ", client.Name())
errs = append(errs, err)
}
// 5 for RcodeRefused in miekg/dns, hardcode to reduce binary size
if err != context.Canceled && err != context.DeadlineExceeded && err != errExpectedIPNonMatch && err != dns.ErrEmptyResponse && dns.RCodeFromError(err) != 5 {
return nil, err
}
}
return nil, errors.New("returning nil for domain ", domain).Base(errors.Combine(errs...))
}
// LookupHosts implements dns.HostsLookup.
func (s *DNS) LookupHosts(domain string) *net.Address {
domain = strings.TrimSuffix(domain, ".")

View File

@@ -29,12 +29,6 @@ type record struct {
AAAA *IPRecord
}
type HTTPSRecord struct {
keypair map[string]string
Expire time.Time
RCode dnsmessage.RCode
}
// IPRecord is a cacheable item for a resolved domain
type IPRecord struct {
ReqID uint16

View File

@@ -23,13 +23,6 @@ type Server interface {
QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns.IPOption, disableCache bool) ([]net.IP, error)
}
// Server is the interface for Enhanced Name Server.
type EnhancedServer interface {
Server
// QueryHTTPS sends HTTPS queries to its configured server.
QueryHTTPS(ctx context.Context, domain string, disableCache bool) (map[string]string, error)
}
// Client is the interface for DNS client.
type Client struct {
server Server

View File

@@ -12,7 +12,6 @@ import (
"sync"
"time"
mdns "github.com/miekg/dns"
utls "github.com/refraction-networking/utls"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/crypto"
@@ -43,8 +42,6 @@ type DoHNameServer struct {
dohURL string
name string
queryStrategy QueryStrategy
HTTPSCache map[string]*HTTPSRecord
}
// NewDoHNameServer creates DOH/DOHL client object for remote/local resolving.
@@ -61,7 +58,6 @@ func NewDoHNameServer(url *url.URL, queryStrategy QueryStrategy, dispatcher rout
name: mode + "//" + url.Host,
dohURL: url.String(),
queryStrategy: queryStrategy,
HTTPSCache: make(map[string]*HTTPSRecord),
}
s.cleanup = &task.Periodic{
Interval: time.Minute,
@@ -211,21 +207,6 @@ func (s *DoHNameServer) updateIP(req *dnsRequest, ipRec *IPRecord) {
common.Must(s.cleanup.Start())
}
func (s *DoHNameServer) updateHTTPS(domain string, HTTPSRec *HTTPSRecord) {
s.Lock()
rec, found := s.HTTPSCache[domain]
if !found {
s.HTTPSCache[domain] = HTTPSRec
}
if found && rec.Expire.Before(time.Now()) {
s.HTTPSCache[domain] = HTTPSRec
}
errors.LogInfo(context.Background(), s.name, " got answer: ", domain, " ", "HTTPS", " -> ", HTTPSRec.keypair)
s.pub.Publish(domain+"HTTPS", nil)
s.Unlock()
}
func (s *DoHNameServer) newReqID() uint16 {
return 0
}
@@ -290,59 +271,6 @@ func (s *DoHNameServer) sendQuery(ctx context.Context, domain string, clientIP n
}
}
func (s *DoHNameServer) sendHTTPSQuery(ctx context.Context, domain string) {
errors.LogInfo(ctx, s.name, " querying HTTPS record for: ", domain)
var deadline time.Time
if d, ok := ctx.Deadline(); ok {
deadline = d
} else {
deadline = time.Now().Add(time.Second * 5)
}
dnsCtx := ctx
// reserve internal dns server requested Inbound
if inbound := session.InboundFromContext(ctx); inbound != nil {
dnsCtx = session.ContextWithInbound(dnsCtx, inbound)
}
dnsCtx = session.ContextWithContent(dnsCtx, &session.Content{
Protocol: "https",
SkipDNSResolve: true,
})
var cancel context.CancelFunc
dnsCtx, cancel = context.WithDeadline(dnsCtx, deadline)
defer cancel()
m := new(mdns.Msg)
m.SetQuestion(mdns.Fqdn(domain), mdns.TypeHTTPS)
m.Id = 0
msg, _ := m.Pack()
response, err := s.dohHTTPSContext(dnsCtx, msg)
if err != nil {
errors.LogError(ctx, err, "failed to retrieve HTTPS query response for ", domain)
return
}
respMsg := new(mdns.Msg)
err = respMsg.Unpack(response)
if err != nil {
errors.LogError(ctx, err, "failed to parse HTTPS query response for ", domain)
return
}
var Record = HTTPSRecord{
keypair: map[string]string{},
}
if len(respMsg.Answer) > 0 {
for _, answer := range respMsg.Answer {
if https, ok := answer.(*mdns.HTTPS); ok && https.Hdr.Name == mdns.Fqdn(domain) {
for _, value := range https.Value {
Record.keypair[value.Key().String()] = value.String()
}
}
}
}
Record.Expire = time.Now().Add(time.Duration(respMsg.Answer[0].Header().Ttl) * time.Second)
s.updateHTTPS(domain, &Record)
}
func (s *DoHNameServer) dohHTTPSContext(ctx context.Context, b []byte) ([]byte, error) {
body := bytes.NewBuffer(b)
req, err := http.NewRequest("POST", s.dohURL, body)
@@ -413,27 +341,6 @@ func (s *DoHNameServer) findIPsForDomain(domain string, option dns_feature.IPOpt
return nil, errRecordNotFound
}
func (s *DoHNameServer) findRecordsForDomain(domain string, Querytype string) (any, error) {
switch Querytype {
case "HTTPS":
s.RLock()
record, found := s.HTTPSCache[domain]
s.RUnlock()
if !found {
return nil, errRecordNotFound
}
if len(record.keypair) == 0 {
return nil, dns_feature.ErrEmptyResponse
}
if record.Expire.Before(time.Now()) {
return nil, errRecordNotFound
}
return record, nil
default:
return nil, errors.New("unsupported query type: " + Querytype)
}
}
// QueryIP implements Server.
func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net.IP, option dns_feature.IPOption, disableCache bool) ([]net.IP, error) { // nolint: dupl
fqdn := Fqdn(domain)
@@ -496,44 +403,3 @@ func (s *DoHNameServer) QueryIP(ctx context.Context, domain string, clientIP net
}
}
}
// QueryHTTPS implements EnhancedServer.
func (s *DoHNameServer) QueryHTTPS(ctx context.Context, domain string, disableCache bool) (map[string]string, error) { // nolint: dupl
fqdn := Fqdn(domain)
if disableCache {
errors.LogDebug(ctx, "DNS cache is disabled. Querying HTTPS for ", domain, " at ", s.name)
} else {
Record, err := s.findRecordsForDomain(fqdn, "HTTPS")
if err == nil || err == dns_feature.ErrEmptyResponse {
errors.LogDebugInner(ctx, err, s.name, " cache HIT ", domain, " -> ", Record.(HTTPSRecord).keypair)
return Record.(HTTPSRecord).keypair, err
}
}
sub := s.pub.Subscribe(fqdn + "HTTPS")
defer sub.Close()
done := make(chan interface{})
go func() {
if sub != nil {
select {
case <-sub.Wait():
case <-ctx.Done():
}
}
close(done)
}()
s.sendHTTPSQuery(ctx, fqdn)
for {
Record, err := s.findRecordsForDomain(fqdn, "HTTPS")
if err != errRecordNotFound {
return Record.(*HTTPSRecord).keypair, err
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-done:
}
}
}

View File

@@ -1,11 +1,13 @@
package crypto
import (
"context"
"encoding/binary"
"io"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
)
// ChunkSizeDecoder is a utility class to decode size value from bytes.
@@ -117,6 +119,7 @@ func (r *ChunkStreamReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
}
r.leftOverSize = size
errors.LogInfo(context.Background(), "StreamReader read ", size)
mb, err := r.reader.ReadAtMost(size)
if !mb.IsEmpty() {
r.leftOverSize -= mb.Len()

View File

@@ -1,6 +1,7 @@
package mux
import (
"context"
"io"
"github.com/xtls/xray-core/common/buf"
@@ -33,6 +34,7 @@ func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) {
}
size, err := serial.ReadUint16(r.reader)
errors.LogInfo(context.Background(), "PacketReader read ", size, r.dest)
if err != nil {
return nil, err
}

View File

@@ -90,7 +90,8 @@ func TestRegressionOutboundLeak(t *testing.T) {
}
{
b := buf.FromBytes([]byte("hello"))
b := buf.New()
b.Write([]byte("hello"))
common.Must(muxClientDownlink.Writer.WriteMultiBuffer(buf.MultiBuffer{b}))
}
@@ -102,7 +103,8 @@ func TestRegressionOutboundLeak(t *testing.T) {
}
{
b := buf.FromBytes([]byte("world"))
b := buf.New()
b.Write([]byte("world"))
common.Must(websiteUplink.Writer.WriteMultiBuffer(buf.MultiBuffer{b}))
}

View File

@@ -1,8 +1,11 @@
package mux
import (
"context"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial"
@@ -75,10 +78,10 @@ func writeMetaWithFrame(writer buf.Writer, meta FrameMetadata, data buf.MultiBuf
if _, err := serial.WriteUint16(frame, uint16(data.Len())); err != nil {
return err
}
mb2 := make(buf.MultiBuffer, 0, len(data)+1)
mb2 = append(mb2, frame)
mb2 = append(mb2, data...)
mb2 = buf.Compact(mb2)
return writer.WriteMultiBuffer(mb2)
}
@@ -106,6 +109,7 @@ func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
mb = mb2
chunk = buf.MultiBuffer{b}
}
errors.LogInfo(context.Background(), "MuxWriter write ", chunk.Len(), w.dest)
if err := w.writeData(chunk); err != nil {
return err
}

View File

@@ -19,7 +19,7 @@ import (
var (
Version_x byte = 25
Version_y byte = 3
Version_z byte = 3
Version_z byte = 6
)
var (

View File

@@ -24,13 +24,6 @@ type Client interface {
LookupIP(domain string, option IPOption) ([]net.IP, error)
}
type EnhancedClient interface {
Client
// LookupHTTPS returns HTTPS records for the given domain.
LookupHTTPS(domain string) (map[string]string, error)
}
type HostsLookup interface {
LookupHosts(domain string) *net.Address
}

16
go.mod
View File

@@ -3,7 +3,7 @@ module github.com/xtls/xray-core
go 1.24
require (
github.com/OmarTariq612/goech v0.0.1
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0
github.com/cloudflare/circl v1.6.0
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344
github.com/golang/mock v1.7.0-rc.1
@@ -22,12 +22,12 @@ require (
github.com/vishvananda/netlink v1.3.0
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/crypto v0.35.0
golang.org/x/net v0.35.0
golang.org/x/sync v0.11.0
golang.org/x/sys v0.30.0
golang.org/x/crypto v0.36.0
golang.org/x/net v0.37.0
golang.org/x/sync v0.12.0
golang.org/x/sys v0.31.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
google.golang.org/grpc v1.70.0
google.golang.org/grpc v1.71.0
google.golang.org/protobuf v1.36.5
gvisor.dev/gvisor v0.0.0-20240320123526-dc6abceb7ff0
h12.io/socks v1.0.3
@@ -51,11 +51,11 @@ require (
go.uber.org/mock v0.5.0 // indirect
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
golang.org/x/mod v0.21.0 // indirect
golang.org/x/text v0.22.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.7.0 // indirect
golang.org/x/tools v0.26.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

54
go.sum
View File

@@ -1,5 +1,5 @@
github.com/OmarTariq612/goech v0.0.1 h1:/0c918Bk1ik65GXDj2k7SOK78DyZr30Jmq9euy1/HXg=
github.com/OmarTariq612/goech v0.0.1/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0 h1:Wo41lDOevRJSGpevP+8Pk5bANX7fJacO2w04aqLiC5I=
github.com/OmarTariq612/goech v0.0.0-20240405204721-8e2e1dafd3a0/go.mod h1:FVGavL/QEBQDcBpr3fAojoK17xX5k9bicBphrOpP7uM=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
@@ -79,24 +79,26 @@ github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZla
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d h1:+B97uD9uHLgAAulhigmys4BVwZZypzK7gPN3WtpgRJg=
github.com/xtls/reality v0.0.0-20240712055506-48f0b2d5ed6d/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
@@ -105,12 +107,12 @@ golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -119,14 +121,14 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -141,10 +143,10 @@ golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeu
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@@ -18,6 +18,7 @@ import (
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/httpupgrade"
"github.com/xtls/xray-core/transport/internet/kcp"
"github.com/xtls/xray-core/transport/internet/quic"
"github.com/xtls/xray-core/transport/internet/reality"
"github.com/xtls/xray-core/transport/internet/splithttp"
"github.com/xtls/xray-core/transport/internet/tcp"
@@ -332,6 +333,22 @@ func (c *SplitHTTPConfig) Build() (proto.Message, error) {
return config, nil
}
type QUICConfig struct {
// Header json.RawMessage `json:"header"`
// Security string `json:"security"`
// Key string `json:"key"`
Fec bool `json:"fec"`
}
// Build implements Buildable.
func (c *QUICConfig) Build() (proto.Message, error) {
config := &quic.Config{
Fec: c.Fec,
}
return config, nil
}
func readFileOrString(f string, s []string) ([]byte, error) {
if len(f) > 0 {
return filesystem.ReadFile(f)
@@ -412,8 +429,6 @@ type TLSConfig struct {
MasterKeyLog string `json:"masterKeyLog"`
ServerNameToVerify string `json:"serverNameToVerify"`
VerifyPeerCertInNames []string `json:"verifyPeerCertInNames"`
ECHConfigList string `json:"echConfigList"`
EchKeySets string `json:"echKeySets"`
}
// Build implements Buildable.
@@ -485,16 +500,6 @@ func (c *TLSConfig) Build() (proto.Message, error) {
}
config.VerifyPeerCertInNames = c.VerifyPeerCertInNames
config.EchConfigList = c.ECHConfigList
if c.EchKeySets != "" {
EchPrivateKey, err := base64.StdEncoding.DecodeString(c.EchKeySets)
if err != nil {
return nil, errors.New("invalid ECH Config", c.EchKeySets)
}
config.EchKeySets = EchPrivateKey
}
return config, nil
}
@@ -695,8 +700,8 @@ func (p TransportProtocol) Build() (string, error) {
return "httpupgrade", nil
case "h2", "h3", "http":
return "", errors.PrintRemovedFeatureError("HTTP transport (without header padding, etc.)", "XHTTP stream-one H2 & H3")
case "quic":
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
case "quic", "datagram":
return "quic", nil
default:
return "", errors.New("Config: unknown transport protocol: ", p)
}
@@ -851,6 +856,7 @@ type StreamConfig struct {
XHTTPSettings *SplitHTTPConfig `json:"xhttpSettings"`
SplitHTTPSettings *SplitHTTPConfig `json:"splithttpSettings"`
KCPSettings *KCPConfig `json:"kcpSettings"`
QUICSettings *QUICConfig `json:"quicSettings"`
GRPCSettings *GRPCConfig `json:"grpcSettings"`
WSSettings *WebSocketConfig `json:"wsSettings"`
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
@@ -942,6 +948,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
Settings: serial.ToTypedMessage(ts),
})
}
if c.QUICSettings != nil {
qs, err := c.QUICSettings.Build()
if err != nil {
return nil, errors.New("Failed to build QUIC config").Base(err)
}
config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
ProtocolName: "quic",
Settings: serial.ToTypedMessage(qs),
})
}
if c.GRPCSettings != nil {
gs, err := c.GRPCSettings.Build()
if err != nil {

View File

@@ -1,8 +1,10 @@
package tls
import (
"encoding/json"
"encoding/pem"
"os"
"strings"
"github.com/OmarTariq612/goech"
"github.com/cloudflare/circl/hpke"
@@ -11,13 +13,13 @@ import (
)
var cmdECH = &base.Command{
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--pem]`,
UsageLine: `{{.Exec}} tls ech [--serverName (string)] [--json]`,
Short: `Generate TLS-ECH certificates`,
Long: `
Generate TLS-ECH certificates.
Set serverName to your custom string: {{.Exec}} tls ech --serverName (string)
Generate into pem format: {{.Exec}} tls ech --pem
Generate into json format: {{.Exec}} tls ech --json
`, // Enable PQ signature schemes: {{.Exec}} tls ech --pq-signature-schemes-enabled
}
@@ -27,7 +29,7 @@ func init() {
var input_pqSignatureSchemesEnabled = cmdECH.Flag.Bool("pqSignatureSchemesEnabled", false, "")
var input_serverName = cmdECH.Flag.String("serverName", "cloudflare-ech.com", "")
var input_pem = cmdECH.Flag.Bool("pem", false, "True == turn on pem output")
var input_json = cmdECH.Flag.Bool("json", false, "True == turn on json output")
func executeECH(cmd *base.Command, args []string) {
var kem hpke.KEM
@@ -38,26 +40,30 @@ func executeECH(cmd *base.Command, args []string) {
kem = hpke.KEM_X25519_HKDF_SHA256
}
echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem, nil)
echKeySet, err := goech.GenerateECHKeySet(0, *input_serverName, kem)
common.Must(err)
// Make single key set to a list with only one element
ECHConfigList := make(goech.ECHConfigList, 1)
ECHConfigList[0] = echKeySet.ECHConfig
ECHKeySetList := make(goech.ECHKeySetList, 1)
ECHKeySetList[0] = echKeySet
configBuffer, _ := ECHConfigList.MarshalBinary()
keyBuffer, _ := ECHKeySetList.MarshalBinary()
configStr, _ := ECHConfigList.ToBase64()
keySetStr, _ := ECHKeySetList.ToBase64()
configBuffer, _ := echKeySet.ECHConfig.MarshalBinary()
keyBuffer, _ := echKeySet.MarshalBinary()
configPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH CONFIGS", Bytes: configBuffer}))
keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "ECH KEYS", Bytes: keyBuffer}))
if *input_pem {
if *input_json {
jECHConfigs := map[string]interface{}{
"configs": strings.Split(strings.TrimSpace(string(configPEM)), "\n"),
}
jECHKey := map[string]interface{}{
"key": strings.Split(strings.TrimSpace(string(keyPEM)), "\n"),
}
for _, i := range []map[string]interface{}{jECHConfigs, jECHKey} {
content, err := json.MarshalIndent(i, "", " ")
common.Must(err)
os.Stdout.Write(content)
os.Stdout.WriteString("\n")
}
} else {
os.Stdout.WriteString(configPEM)
os.Stdout.WriteString(keyPEM)
} else {
os.Stdout.WriteString("ECH config list: \n" + configStr + "\n")
os.Stdout.WriteString("ECH Key sets: \n" + keySetStr + "\n")
}
}

View File

@@ -53,6 +53,7 @@ import (
_ "github.com/xtls/xray-core/transport/internet/grpc"
_ "github.com/xtls/xray-core/transport/internet/httpupgrade"
_ "github.com/xtls/xray-core/transport/internet/kcp"
_ "github.com/xtls/xray-core/transport/internet/quic"
_ "github.com/xtls/xray-core/transport/internet/reality"
_ "github.com/xtls/xray-core/transport/internet/splithttp"
_ "github.com/xtls/xray-core/transport/internet/tcp"

View File

@@ -106,14 +106,6 @@ func lookupIP(domain string, strategy DomainStrategy, localAddr net.Address) ([]
return ips, err
}
func LookupHTTPS(domain string) (map[string]string, error) {
if dnsClient == nil {
return nil, nil
}
HTTPSRecord, err := dnsClient.(dns.EnhancedClient).LookupHTTPS(domain)
return HTTPSRecord, err
}
func canLookupIP(ctx context.Context, dst net.Destination, sockopt *SocketConfig) bool {
if dst.Address.Family().IsIP() || dnsClient == nil {
return false

View File

@@ -0,0 +1,137 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.35.1
// protoc v5.28.2
// source: transport/internet/quic/config.proto
package quic
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 Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// string key = 1;
// xray.common.protocol.SecurityConfig security = 2;
// xray.common.serial.TypedMessage header = 3;
Fec bool `protobuf:"varint,4,opt,name=fec,proto3" json:"fec,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
mi := &file_transport_internet_quic_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_quic_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_quic_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetFec() bool {
if x != nil {
return x.Fec
}
return false
}
var File_transport_internet_quic_config_proto protoreflect.FileDescriptor
var file_transport_internet_quic_config_proto_rawDesc = []byte{
0x0a, 0x24, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2f, 0x71, 0x75, 0x69, 0x63, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1c, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61,
0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e,
0x71, 0x75, 0x69, 0x63, 0x22, 0x1a, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x10,
0x0a, 0x03, 0x66, 0x65, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x66, 0x65, 0x63,
0x42, 0x76, 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61,
0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e,
0x71, 0x75, 0x69, 0x63, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72,
0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2f, 0x71, 0x75, 0x69, 0x63, 0xaa, 0x02, 0x1c, 0x58, 0x72, 0x61, 0x79,
0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72,
0x6e, 0x65, 0x74, 0x2e, 0x51, 0x75, 0x69, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_transport_internet_quic_config_proto_rawDescOnce sync.Once
file_transport_internet_quic_config_proto_rawDescData = file_transport_internet_quic_config_proto_rawDesc
)
func file_transport_internet_quic_config_proto_rawDescGZIP() []byte {
file_transport_internet_quic_config_proto_rawDescOnce.Do(func() {
file_transport_internet_quic_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_quic_config_proto_rawDescData)
})
return file_transport_internet_quic_config_proto_rawDescData
}
var file_transport_internet_quic_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_transport_internet_quic_config_proto_goTypes = []any{
(*Config)(nil), // 0: xray.transport.internet.quic.Config
}
var file_transport_internet_quic_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_internet_quic_config_proto_init() }
func file_transport_internet_quic_config_proto_init() {
if File_transport_internet_quic_config_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_transport_internet_quic_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_transport_internet_quic_config_proto_goTypes,
DependencyIndexes: file_transport_internet_quic_config_proto_depIdxs,
MessageInfos: file_transport_internet_quic_config_proto_msgTypes,
}.Build()
File_transport_internet_quic_config_proto = out.File
file_transport_internet_quic_config_proto_rawDesc = nil
file_transport_internet_quic_config_proto_goTypes = nil
file_transport_internet_quic_config_proto_depIdxs = nil
}

View File

@@ -0,0 +1,14 @@
syntax = "proto3";
package xray.transport.internet.quic;
option csharp_namespace = "Xray.Transport.Internet.Quic";
option go_package = "github.com/xtls/xray-core/transport/internet/quic";
option java_package = "com.xray.transport.internet.quic";
option java_multiple_files = true;
message Config {
// string key = 1;
// xray.common.protocol.SecurityConfig security = 2;
// xray.common.serial.TypedMessage header = 3;
bool fec = 4;
}

View File

@@ -0,0 +1,247 @@
package quic
import (
"context"
"time"
"github.com/quic-go/quic-go"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/mux"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/common/signal/done"
)
var MaxIncomingStreams = 16
var currentStream = 0
type interConn struct {
ctx context.Context
quicConn quic.Connection // small udp packet can be sent with Datagram directly
streams []quic.Stream // other packets can be sent via steam, it offer mux, reliability, fragmentation and ordering
readChannel chan readResult
reader buf.MultiBufferContainer
done *done.Instance
local net.Addr
remote net.Addr
}
type readResult struct {
buffer []byte
err error
}
func NewConnInitReader(ctx context.Context, quicConn quic.Connection, done *done.Instance, remote net.Addr) *interConn {
c := &interConn{
ctx: ctx,
quicConn: quicConn,
readChannel: make(chan readResult),
reader: buf.MultiBufferContainer{},
done: done,
local: quicConn.LocalAddr(),
remote: remote,
}
go func() {
for {
received, e := c.quicConn.ReceiveDatagram(c.ctx)
errors.LogInfo(c.ctx, "Read ReceiveDatagram ", len(received))
c.readChannel <- readResult{buffer: received, err: e}
}
}()
go c.acceptStreams()
return c
}
func (c *interConn) acceptStreams() {
for {
stream, err := c.quicConn.AcceptStream(context.Background())
errors.LogInfo(c.ctx, "Read AcceptStream ", err)
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to accept stream")
select {
case <-c.quicConn.Context().Done():
return
case <-c.done.Wait():
if err := c.quicConn.CloseWithError(0, ""); err != nil {
errors.LogInfoInner(context.Background(), err, "failed to close connection")
}
return
default:
time.Sleep(time.Second)
continue
}
}
go c.readMuxCoolPacket(stream)
c.streams = append(c.streams, stream)
}
}
func (c *interConn) readMuxCoolPacket(stream quic.Stream) {
for {
received := make([]byte, buf.Size)
i, e := stream.Read(received)
if e != nil {
errors.LogErrorInner(c.ctx, e, "Error read stream, drop this buffer ", i)
c.readChannel <- readResult{buffer: nil, err: e}
continue;
}
errors.LogInfo(c.ctx, "Read stream ", i)
buffer := buf.New()
buffer.Write(received[:i])
muxCoolReader := &buf.MultiBufferContainer{}
muxCoolReader.MultiBuffer = append(muxCoolReader.MultiBuffer, buffer)
var meta mux.FrameMetadata
err := meta.Unmarshal(muxCoolReader)
if err != nil {
errors.LogInfo(c.ctx, "Not a Mux Cool packet beginning, copy directly ", i)
buf.ReleaseMulti(muxCoolReader.MultiBuffer)
c.readChannel <- readResult{buffer: received[:i], err: e}
continue;
}
if !meta.Option.Has(mux.OptionData) {
errors.LogInfo(c.ctx, "No option data, copy directly ", i)
buf.ReleaseMulti(muxCoolReader.MultiBuffer)
c.readChannel <- readResult{buffer: received[:i], err: e}
continue;
}
size, err := serial.ReadUint16(muxCoolReader)
remaining := uint16(muxCoolReader.MultiBuffer.Len())
errors.LogInfo(c.ctx, "Read stream ", i, " option size ", size, " remaining size ", remaining)
if err != nil || size <= remaining || size > remaining + 1500 {
errors.LogInfo(c.ctx, "do not wait for second part of UDP packet ", i)
buf.ReleaseMulti(muxCoolReader.MultiBuffer)
c.readChannel <- readResult{buffer: received[:i], err: e}
continue;
}
i2, e := stream.Read(received[i:])
if e != nil {
errors.LogErrorInner(c.ctx, e, "Error read stream, drop this buffer ", i2)
buf.ReleaseMulti(muxCoolReader.MultiBuffer)
c.readChannel <- readResult{buffer: nil, err: e}
continue;
}
errors.LogInfo(c.ctx, "Read stream i2 size ", i2)
buf.ReleaseMulti(muxCoolReader.MultiBuffer)
c.readChannel <- readResult{buffer: received[:(i + i2)], err: e}
}
}
func (c *interConn) Read(b []byte) (int, error) {
if c.reader.MultiBuffer.Len() > 0 {
return c.reader.Read(b)
}
received := <- c.readChannel
if received.err != nil {
return 0, received.err
}
buffer := buf.New()
buffer.Write(received.buffer)
c.reader.MultiBuffer = append(c.reader.MultiBuffer, buffer)
errors.LogInfo(c.ctx, "Read copy ", len(received.buffer))
return c.reader.Read(b)
}
func (c *interConn) WriteMultiBuffer(mb buf.MultiBuffer) error {
mb = buf.Compact(mb)
mb, err := buf.WriteMultiBuffer(c, mb)
buf.ReleaseMulti(mb)
return err
}
func (c *interConn) Write(b []byte) (int, error) {
if len(b) > 1240 { // TODO: why quic-go increase internal MTU causing packet loss?
if len(c.streams) < MaxIncomingStreams {
stream, err := c.quicConn.OpenStream()
errors.LogInfo(c.ctx, "Write OpenStream ", err)
if err == nil {
c.streams = append(c.streams, stream)
} else {
errors.LogInfoInner(c.ctx, err, "failed to openStream: ")
}
}
currentStream++;
if currentStream > len(c.streams) - 1 {
currentStream = 0;
}
errors.LogInfo(c.ctx, "Write stream ", len(b), currentStream, len(c.streams))
return c.streams[currentStream].Write(b)
}
var err = c.quicConn.SendDatagram(b)
errors.LogInfo(c.ctx, "Write SendDatagram ", len(b), err)
if _, ok := err.(*quic.DatagramTooLargeError); ok {
if len(c.streams) < MaxIncomingStreams {
stream, err := c.quicConn.OpenStream()
errors.LogInfo(c.ctx, "Write OpenStream ", err)
if err == nil {
c.streams = append(c.streams, stream)
} else {
errors.LogInfoInner(c.ctx, err, "failed to openStream: ")
}
}
currentStream++;
if currentStream > len(c.streams) - 1 {
currentStream = 0;
}
errors.LogInfo(c.ctx, "Write stream ", len(b), currentStream, len(c.streams))
return c.streams[currentStream].Write(b)
}
if err != nil {
return 0, err
}
return len(b), nil
}
func (c *interConn) Close() error {
var err error
for _, s := range c.streams {
e := s.Close()
if e != nil {
err = e
}
}
return err
}
func (c *interConn) LocalAddr() net.Addr {
return c.local
}
func (c *interConn) RemoteAddr() net.Addr {
return c.remote
}
func (c *interConn) SetDeadline(t time.Time) error {
var err error
for _, s := range c.streams {
e := s.SetDeadline(t)
if e != nil {
err = e
}
}
return err
}
func (c *interConn) SetReadDeadline(t time.Time) error {
var err error
for _, s := range c.streams {
e := s.SetReadDeadline(t)
if e != nil {
err = e
}
}
return err
}
func (c *interConn) SetWriteDeadline(t time.Time) error {
var err error
for _, s := range c.streams {
e := s.SetWriteDeadline(t)
if e != nil {
err = e
}
}
return err
}

View File

@@ -0,0 +1,95 @@
package quic
import (
"context"
"time"
"github.com/quic-go/quic-go"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
)
func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
if tlsConfig == nil {
tlsConfig = &tls.Config{
ServerName: internalDomain,
AllowInsecure: true,
}
}
var destAddr *net.UDPAddr
if dest.Address.Family().IsIP() {
destAddr = &net.UDPAddr{
IP: dest.Address.IP(),
Port: int(dest.Port),
}
} else {
dialerIp := internet.DestIpAddress()
if dialerIp != nil {
destAddr = &net.UDPAddr{
IP: dialerIp,
Port: int(dest.Port),
}
errors.LogInfo(ctx, "quic Dial use dialer dest addr: ", destAddr)
} else {
addr, err := net.ResolveUDPAddr("udp", dest.NetAddr())
if err != nil {
return nil, err
}
destAddr = addr
}
}
config := streamSettings.ProtocolSettings.(*Config)
return openConnection(ctx, destAddr, config, tlsConfig, streamSettings.SocketSettings)
}
func openConnection(ctx context.Context, destAddr net.Addr, config *Config, tlsConfig *tls.Config, sockopt *internet.SocketConfig) (stat.Connection, error) {
dest := net.DestinationFromAddr(destAddr)
errors.LogInfo(ctx, "dialing quic to ", dest)
rawConn, err := internet.DialSystem(ctx, dest, sockopt)
if err != nil {
return nil, errors.New("failed to dial to dest: ", err).AtWarning().Base(err)
}
quicConfig := &quic.Config{
KeepAlivePeriod: 0,
HandshakeIdleTimeout: time.Second * 8,
MaxIdleTimeout: time.Second * 300,
EnableDatagrams: true,
}
var udpConn *net.UDPConn
switch conn := rawConn.(type) {
case *net.UDPConn:
udpConn = conn
case *internet.PacketConnWrapper:
udpConn = conn.Conn.(*net.UDPConn)
default:
rawConn.Close()
return nil, errors.New("QUIC with sockopt is unsupported").AtWarning()
}
tr := quic.Transport{
ConnectionIDLength: 12,
Conn: udpConn,
}
conn, err := tr.Dial(context.Background(), destAddr, tlsConfig.GetTLSConfig(tls.WithDestination(dest)), quicConfig)
if err != nil {
udpConn.Close()
return nil, err
}
return NewConnInitReader(ctx, conn, done.New(), destAddr), nil
}
func init() {
common.Must(internet.RegisterTransportDialer(protocolName, Dial))
}

View File

@@ -0,0 +1,108 @@
package quic
import (
"context"
"time"
"github.com/quic-go/quic-go"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol/tls/cert"
"github.com/xtls/xray-core/common/signal/done"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/tls"
)
// Listener is an internet.Listener that listens for TCP connections.
type Listener struct {
rawConn *net.UDPConn
listener *quic.Listener
done *done.Instance
addConn internet.ConnHandler
}
func (l *Listener) keepAccepting(ctx context.Context) {
for {
conn, err := l.listener.Accept(context.Background())
if err != nil {
errors.LogInfoInner(context.Background(), err, "failed to accept QUIC connection")
if l.done.Done() {
break
}
time.Sleep(time.Second)
continue
}
l.addConn(NewConnInitReader(ctx, conn, l.done, conn.RemoteAddr()))
}
}
// Addr implements internet.Listener.Addr.
func (l *Listener) Addr() net.Addr {
return l.listener.Addr()
}
// Close implements internet.Listener.Close.
func (l *Listener) Close() error {
l.done.Close()
l.listener.Close()
l.rawConn.Close()
return nil
}
// Listen creates a new Listener based on configurations.
func Listen(ctx context.Context, address net.Address, port net.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
if address.Family().IsDomain() {
return nil, errors.New("domain address is not allows for listening quic")
}
tlsConfig := tls.ConfigFromStreamSettings(streamSettings)
if tlsConfig == nil {
tlsConfig = &tls.Config{
Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil, cert.DNSNames(internalDomain), cert.CommonName(internalDomain)))},
}
}
//config := streamSettings.ProtocolSettings.(*Config)
rawConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{
IP: address.IP(),
Port: int(port),
}, streamSettings.SocketSettings)
if err != nil {
return nil, err
}
quicConfig := &quic.Config{
KeepAlivePeriod: 0,
HandshakeIdleTimeout: time.Second * 8,
MaxIdleTimeout: time.Second * 300,
MaxIncomingStreams: 16,
MaxIncomingUniStreams: -1,
EnableDatagrams: true,
}
tr := quic.Transport{
ConnectionIDLength: 12,
Conn: rawConn.(*net.UDPConn),
}
qListener, err := tr.Listen(tlsConfig.GetTLSConfig(), quicConfig)
if err != nil {
rawConn.Close()
return nil, err
}
listener := &Listener{
done: done.New(),
rawConn: rawConn.(*net.UDPConn),
listener: qListener,
addConn: handler,
}
go listener.keepAccepting(ctx)
return listener, nil
}
func init() {
common.Must(internet.RegisterTransportListener(protocolName, Listen))
}

View File

@@ -0,0 +1,17 @@
package quic
import (
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/transport/internet"
)
const (
protocolName = "quic"
internalDomain = "quic.internal.example.com"
)
func init() {
common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
return new(Config)
}))
}

View File

@@ -0,0 +1,105 @@
package quic_test
import (
"context"
"crypto/rand"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/xtls/xray-core/common"
"github.com/xtls/xray-core/common/buf"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/common/protocol/tls/cert"
"github.com/xtls/xray-core/testing/servers/udp"
"github.com/xtls/xray-core/transport/internet"
"github.com/xtls/xray-core/transport/internet/quic"
"github.com/xtls/xray-core/transport/internet/stat"
"github.com/xtls/xray-core/transport/internet/tls"
)
func TestShortQuicConnection(t *testing.T) {
testQuicConnection(t, 1024)
}
func TestAroundMTUQuicConnection(t *testing.T) {
testQuicConnection(t, 1247)
}
func TestLongQuicConnection(t *testing.T) {
testQuicConnection(t, 1500)
}
func testQuicConnection(t *testing.T, dataLen int32) {
port := udp.PickPort()
listener, err := quic.Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{
ProtocolName: "quic",
ProtocolSettings: &quic.Config{},
SecurityType: "tls",
SecuritySettings: &tls.Config{
Certificate: []*tls.Certificate{
tls.ParseCertificate(
cert.MustGenerate(nil,
cert.DNSNames("www.example.com"),
),
),
},
},
}, func(conn stat.Connection) {
go func() {
defer conn.Close()
b := buf.New()
defer b.Release()
for {
b.Clear()
if _, err := b.ReadFrom(conn); err != nil {
return
}
common.Must2(conn.Write(b.Bytes()))
}
}()
})
common.Must(err)
defer listener.Close()
time.Sleep(time.Second)
dctx := context.Background()
conn, err := quic.Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{
ProtocolName: "quic",
ProtocolSettings: &quic.Config{},
SecurityType: "tls",
SecuritySettings: &tls.Config{
ServerName: "www.example.com",
AllowInsecure: true,
},
})
common.Must(err)
defer conn.Close()
b1 := make([]byte, dataLen)
common.Must2(rand.Read(b1))
b2 := buf.New()
common.Must2(conn.Write(b1))
b2.Clear()
common.Must2(b2.ReadFullFrom(conn, dataLen))
if r := cmp.Diff(b2.Bytes(), b1); r != "" {
t.Error(r)
}
time.Sleep(1000 * time.Millisecond)
common.Must2(conn.Write(b1))
b2.Clear()
common.Must2(b2.ReadFullFrom(conn, dataLen))
if r := cmp.Diff(b2.Bytes(), b1); r != "" {
t.Error(r)
}
}

View File

@@ -444,12 +444,6 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
config.KeyLogWriter = writer
}
}
if len(c.EchConfigList) > 0 || len(c.EchKeySets) > 0 {
err := ApplyECH(c, config)
if err != nil {
errors.LogError(context.Background(), err)
}
}
return config
}

View File

@@ -217,8 +217,6 @@ type Config struct {
// @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried.
// @Critical
VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"`
EchConfigList string `protobuf:"bytes,18,opt,name=ech_config_list,json=echConfigList,proto3" json:"ech_config_list,omitempty"`
EchKeySets []byte `protobuf:"bytes,19,opt,name=ech_key_sets,json=echKeySets,proto3" json:"ech_key_sets,omitempty"`
}
func (x *Config) Reset() {
@@ -363,20 +361,6 @@ func (x *Config) GetVerifyPeerCertInNames() []string {
return nil
}
func (x *Config) GetEchConfigList() string {
if x != nil {
return x.EchConfigList
}
return ""
}
func (x *Config) GetEchKeySets() []byte {
if x != nil {
return x.EchKeySets
}
return nil
}
var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
var file_transport_internet_tls_config_proto_rawDesc = []byte{
@@ -408,7 +392,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a,
0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46,
0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xe4, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x9a, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73,
0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c,
0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65,
@@ -458,19 +442,15 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f,
0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15,
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x65, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
0x65, 0x63, 0x68, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x20, 0x0a,
0x0c, 0x65, 0x63, 0x68, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x13, 0x20,
0x01, 0x28, 0x0c, 0x52, 0x0a, 0x65, 0x63, 0x68, 0x4b, 0x65, 0x79, 0x53, 0x65, 0x74, 0x73, 0x42,
0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e,
0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74,
0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f,
0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72,
0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74,
0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58,
0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (

View File

@@ -91,8 +91,4 @@ message Config {
@Critical
*/
repeated string verify_peer_cert_in_names = 17;
string ech_config_list = 18;
bytes ech_key_sets = 19;
}

View File

@@ -1,255 +0,0 @@
package tls
import (
"bytes"
"context"
"crypto/tls"
"io"
"net/http"
"strings"
"sync"
"time"
"github.com/OmarTariq612/goech"
"github.com/miekg/dns"
"github.com/xtls/xray-core/common/errors"
"github.com/xtls/xray-core/common/net"
"github.com/xtls/xray-core/transport/internet"
)
func ApplyECH(c *Config, config *tls.Config) error {
var ECHConfig []byte
var err error
nameToQuery := c.ServerName
var DNSServer string
// for client
if len(c.EchConfigList) != 0 {
// direct base64 config
if strings.HasPrefix(c.EchConfigList, "base64") {
Base64ECHConfigList := c.EchConfigList[len("base64://"):]
ECHConfigList, err := goech.ECHConfigListFromBase64(Base64ECHConfigList)
if err != nil {
return errors.New("Failed to unmarshal ECHConfigList: ", err)
}
ECHConfig, _ = ECHConfigList.MarshalBinary()
} else { // query config from dns
parts := strings.Split(c.EchConfigList, "+")
if len(parts) == 2 {
// parse ECH DNS server in format of "example.com+https://1.1.1.1/dns-query"
nameToQuery = parts[0]
DNSServer = parts[1]
} else if len(parts) == 1 {
// normal format
DNSServer = parts[0]
} else {
return errors.New("Invalid ECH DNS server format: ", c.EchConfigList)
}
if nameToQuery == "" {
return errors.New("Using DNS for ECH Config needs serverName or use Server format example.com+https://1.1.1.1/dns-query")
}
ECHConfig, err = QueryRecord(nameToQuery, DNSServer)
if err != nil {
return err
}
}
config.EncryptedClientHelloConfigList = ECHConfig
}
// for server
if len(c.EchKeySets) != 0 {
var keys []tls.EncryptedClientHelloKey
KeySets, err := goech.UnmarshalECHKeySetList(c.EchKeySets)
if err != nil {
return errors.New("Failed to unmarshal ECHKeySetList: ", err)
}
for idx, keySet := range KeySets {
ECHConfig, err := keySet.ECHConfig.MarshalBinary()
ECHPrivateKey, err := keySet.PrivateKey.MarshalBinary()
if err != nil {
return errors.New("Failed to marshal ECHKey in index: ", idx, "with err: ", err)
}
keys = append(keys, tls.EncryptedClientHelloKey{
Config: ECHConfig,
PrivateKey: ECHPrivateKey})
}
config.EncryptedClientHelloKeys = keys
}
return nil
}
type record struct {
echConfig []byte
expire time.Time
}
var (
dnsCache sync.Map
// global Lock? I'm not sure if this needs finer grained locks.
// If we do this, we will need to nest another layer of struct
updating sync.Mutex
)
// QueryRecord returns the ECH config for given domain.
// If the record is not in cache or expired, it will query the DNS server and update the cache.
func QueryRecord(domain string, server string) ([]byte, error) {
val, found := dnsCache.Load(domain)
rec, _ := val.(record)
if found && rec.expire.After(time.Now()) {
errors.LogDebug(context.Background(), "Cache hit for domain: ", domain)
return rec.echConfig, nil
}
updating.Lock()
defer updating.Unlock()
// Try to get cache again after lock, in case another goroutine has updated it
// This might happen when the core tring is just stared and multiple goroutines are trying to query the same domain
val, found = dnsCache.Load(domain)
rec, _ = val.(record)
if found && rec.expire.After(time.Now()) {
errors.LogDebug(context.Background(), "ECH Config cache hit for domain: ", domain, " after trying to get update lock")
return rec.echConfig, nil
}
// Query ECH config from DNS server
errors.LogDebug(context.Background(), "Trying to query ECH config for domain: ", domain, " with ECH server: ", server)
echConfig, ttl, err := dnsQuery(server, domain)
if err != nil {
return []byte{}, err
}
// Set minimum TTL to 600 seconds
if ttl < 600 {
ttl = 600
}
// Update cache
newRecored := record{
echConfig: echConfig,
expire: time.Now().Add(time.Second * time.Duration(ttl)),
}
dnsCache.Store(domain, newRecored)
return echConfig, nil
}
// dnsQuery is the real func for sending type65 query for given domain to given DNS server.
// return ECH config, TTL and error
func dnsQuery(server string, domain string) ([]byte, uint32, error) {
if server == "xray" {
HTTPSRecord, err := internet.LookupHTTPS(domain)
if err !=nil {
return []byte{}, 0, errors.New("failed to lookup HTTPS record with xray internal DNS: ", err)
}
ECH := HTTPSRecord["ech"]
if ECH == "" {
return []byte{}, 0, errors.New("no ech record found")
}
Base64echConfigList, err := goech.ECHConfigListFromBase64(ECH)
if err != nil {
return []byte{}, 0, errors.New("failed to unmarshal ECHConfigList: ", err)
}
echConfigList, _ := Base64echConfigList.MarshalBinary()
return echConfigList, 600, nil
}
m := new(dns.Msg)
var dnsResolve []byte
m.SetQuestion(dns.Fqdn(domain), dns.TypeHTTPS)
// for DOH server
if strings.HasPrefix(server, "https://") {
// always 0 in DOH
m.Id = 0
msg, err := m.Pack()
if err != nil {
return []byte{}, 0, err
}
// All traffic sent by core should via xray's internet.DialSystem
// This involves the behavior of some Android VPN GUI clients
tr := &http.Transport{
IdleConnTimeout: 90 * time.Second,
ForceAttemptHTTP2: true,
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
dest, err := net.ParseDestination(network + ":" + addr)
if err != nil {
return nil, err
}
conn, err := internet.DialSystem(ctx, dest, nil)
if err != nil {
return nil, err
}
return conn, nil
},
}
client := &http.Client{
Timeout: 5 * time.Second,
Transport: tr,
}
req, err := http.NewRequest("POST", server, bytes.NewReader(msg))
if err != nil {
return []byte{}, 0, err
}
req.Header.Set("Content-Type", "application/dns-message")
resp, err := client.Do(req)
if err != nil {
return []byte{}, 0, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return []byte{}, 0, err
}
if resp.StatusCode != http.StatusOK {
return []byte{}, 0, errors.New("query failed with response code:", resp.StatusCode)
}
dnsResolve = respBody
} else if strings.HasPrefix(server, "udp://") { // for classic udp dns server
udpServerAddr := server[len("udp://"):]
// default port 53 if not specified
if !strings.Contains(udpServerAddr, ":") {
udpServerAddr = udpServerAddr + ":53"
}
dest, err := net.ParseDestination("udp" + ":" + udpServerAddr)
if err != nil {
return nil, 0, errors.New("failed to parse udp dns server ", udpServerAddr, " for ECH: ", err)
}
dnsTimeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// use xray's internet.DialSystem as mentioned above
conn, err := internet.DialSystem(dnsTimeoutCtx, dest, nil)
defer conn.Close()
if err != nil {
return []byte{}, 0, err
}
msg, err := m.Pack()
if err != nil {
return []byte{}, 0, err
}
conn.Write(msg)
udpResponse := make([]byte, 512)
_, err = conn.Read(udpResponse)
if err != nil {
return []byte{}, 0, err
}
dnsResolve = udpResponse
}
respMsg := new(dns.Msg)
err := respMsg.Unpack(dnsResolve)
if err != nil {
return []byte{}, 0, errors.New("failed to unpack dns response for ECH: ", err)
}
if len(respMsg.Answer) > 0 {
for _, answer := range respMsg.Answer {
if https, ok := answer.(*dns.HTTPS); ok && https.Hdr.Name == dns.Fqdn(domain) {
for _, v := range https.Value {
if echConfig, ok := v.(*dns.SVCBECHConfig); ok {
errors.LogDebug(context.Background(), "Get ECH config:", echConfig.String(), " TTL:", respMsg.Answer[0].Header().Ttl)
return echConfig.ECH, answer.Header().Ttl, nil
}
}
}
}
}
return []byte{}, 0, errors.New("no ech record found")
}