diff --git a/go.mod b/go.mod index 41751d2f7..d6cdad060 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ toolchain go1.22.7 require ( github.com/adrg/xdg v0.5.3 + github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/render v1.0.3 github.com/go-playground/validator/v10 v10.22.1 @@ -28,6 +29,7 @@ require ( github.com/stretchr/testify v1.9.0 github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 + github.com/v2fly/hysteria/core/v2 v2.0.0-20250113081444-b0a0747ac7ab github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e github.com/v2fly/struc v0.0.0-20241227015403-8e8fa1badfd6 github.com/vincent-petithory/dataurl v1.0.0 @@ -65,11 +67,9 @@ require ( github.com/klauspost/compress v1.17.4 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/klauspost/reedsolomon v1.11.7 // indirect - github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect github.com/mustafaturan/monoton v1.0.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect github.com/onsi/ginkgo/v2 v2.17.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pion/logging v0.2.2 // indirect @@ -77,8 +77,10 @@ require ( github.com/pion/sctp v1.8.7 // indirect github.com/pion/transport/v3 v3.0.7 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/quic-go/qpack v0.5.1 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/xtaci/smux v1.5.24 // indirect go.uber.org/mock v0.4.0 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect @@ -87,5 +89,4 @@ require ( golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect ) diff --git a/go.sum b/go.sum index 0fd5e7cd8..1ecf464e8 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7 h1:zO38yBOvQ1dLHbSuaU5BFZ8zalnSDQslj+i/9AGOk9s= +github.com/apernet/quic-go v0.48.2-0.20241104191913-cb103fcecfe7/go.mod h1:LoSUY2chVqNQCDyi4IZGqPpXLy1FuCkE37PKwtJvNGg= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= @@ -50,7 +52,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -189,10 +190,11 @@ github.com/klauspost/reedsolomon v1.11.7/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lunixbochs/struc v0.0.0-20190916212049-a5c72983bc42/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= @@ -223,8 +225,6 @@ github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo/v2 v2.17.0 h1:kdnunFXpBjbzN56hcJHrXZ8M+LOkenKA7NnBzTNigTI= github.com/onsi/ginkgo/v2 v2.17.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= @@ -273,6 +273,8 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE= github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM= @@ -281,6 +283,8 @@ github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= @@ -309,6 +313,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -329,6 +335,8 @@ github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/ github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08/go.mod h1:KAuQNm+LWQCOFqdBcUgihPzRpVXRKzGbTNhfEfRZ4wY= github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 h1:p1UzXK6VAutXFFQMnre66h7g1BjRKUnLv0HfmmRoz7w= github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848/go.mod h1:p80Bv154ZtrGpXMN15slDCqc9UGmfBuUzheDFBYaW/M= +github.com/v2fly/hysteria/core/v2 v2.0.0-20250113081444-b0a0747ac7ab h1:GstVKviVuxRZXxHzeWq0N2M4LG5A5W1HvFX1b7aQ48w= +github.com/v2fly/hysteria/core/v2 v2.0.0-20250113081444-b0a0747ac7ab/go.mod h1:yWDV7zOoL0pPhVlWV6Hqf46gWYenwwT9g4Y+e5yPRz8= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/v2fly/struc v0.0.0-20241227015403-8e8fa1badfd6 h1:Qea2jW7g1hvQ9TkYq3aT2h0NDWjPQHtvDfmKXoWgJ9E= @@ -352,6 +360,8 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.starlark.net v0.0.0-20230612165344-9532f5667272 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8= go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= @@ -572,8 +582,8 @@ gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLks gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= diff --git a/infra/conf/v4/hysteria2.go b/infra/conf/v4/hysteria2.go new file mode 100644 index 000000000..690513be7 --- /dev/null +++ b/infra/conf/v4/hysteria2.go @@ -0,0 +1,78 @@ +package v4 + +import ( + "github.com/golang/protobuf/proto" + + "github.com/v2fly/v2ray-core/v5/common/net/packetaddr" + "github.com/v2fly/v2ray-core/v5/common/protocol" + "github.com/v2fly/v2ray-core/v5/common/serial" + "github.com/v2fly/v2ray-core/v5/infra/conf/cfgcommon" + "github.com/v2fly/v2ray-core/v5/proxy/hysteria2" +) + +// Hysteria2ServerTarget is configuration of a single hysteria2 server +type Hysteria2ServerTarget struct { + Address *cfgcommon.Address `json:"address"` + Port uint16 `json:"port"` + Email string `json:"email"` + Level byte `json:"level"` +} + +// Hysteria2ClientConfig is configuration of hysteria2 servers +type Hysteria2ClientConfig struct { + Servers []*Hysteria2ServerTarget `json:"servers"` +} + +// Build implements Buildable +func (c *Hysteria2ClientConfig) Build() (proto.Message, error) { + config := new(hysteria2.ClientConfig) + + if len(c.Servers) == 0 { + return nil, newError("0 Hysteria2 server configured.") + } + + serverSpecs := make([]*protocol.ServerEndpoint, len(c.Servers)) + for idx, rec := range c.Servers { + if rec.Address == nil { + return nil, newError("Hysteria2 server address is not set.") + } + if rec.Port == 0 { + return nil, newError("Invalid Hysteria2 port.") + } + account := &hysteria2.Account{} + hysteria2 := &protocol.ServerEndpoint{ + Address: rec.Address.Build(), + Port: uint32(rec.Port), + User: []*protocol.User{ + { + Level: uint32(rec.Level), + Email: rec.Email, + Account: serial.ToTypedMessage(account), + }, + }, + } + + serverSpecs[idx] = hysteria2 + } + + config.Server = serverSpecs + + return config, nil +} + +// Hysteria2ServerConfig is Inbound configuration +type Hysteria2ServerConfig struct { + PacketEncoding string `json:"packetEncoding"` +} + +// Build implements Buildable +func (c *Hysteria2ServerConfig) Build() (proto.Message, error) { + config := new(hysteria2.ServerConfig) + switch c.PacketEncoding { + case "Packet": + config.PacketEncoding = packetaddr.PacketAddrType_Packet + case "", "None": + config.PacketEncoding = packetaddr.PacketAddrType_None + } + return config, nil +} diff --git a/infra/conf/v4/transport_internet.go b/infra/conf/v4/transport_internet.go index 5612bf986..61412d958 100644 --- a/infra/conf/v4/transport_internet.go +++ b/infra/conf/v4/transport_internet.go @@ -16,6 +16,7 @@ import ( "github.com/v2fly/v2ray-core/v5/transport/internet/domainsocket" httpheader "github.com/v2fly/v2ray-core/v5/transport/internet/headers/http" "github.com/v2fly/v2ray-core/v5/transport/internet/http" + "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" "github.com/v2fly/v2ray-core/v5/transport/internet/kcp" "github.com/v2fly/v2ray-core/v5/transport/internet/quic" "github.com/v2fly/v2ray-core/v5/transport/internet/tcp" @@ -143,6 +144,26 @@ type Hy2ConfigCongestion struct { DownMbps uint64 `json:"down_mbps"` } +type Hy2Config struct { + Password string `json:"password"` + Congestion Hy2ConfigCongestion `json:"congestion"` + UseUdpExtension bool `json:"use_udp_extension"` + IgnoreClientBandwidth bool `json:"ignore_client_bandwidth"` +} + +// Build implements Buildable. +func (c *Hy2Config) Build() (proto.Message, error) { + return &hysteria2.Config{Password: c.Password, + Congestion: &hysteria2.Congestion{ + Type: c.Congestion.Type, + DownMbps: c.Congestion.DownMbps, + UpMbps: c.Congestion.UpMbps, + }, + UseUdpExtension: c.UseUdpExtension, + IgnoreClientBandwidth: c.IgnoreClientBandwidth, + }, nil +} + type WebSocketConfig struct { Path string `json:"path"` Headers map[string]string `json:"headers"` @@ -285,6 +306,8 @@ func (p TransportProtocol) Build() (string, error) { return "quic", nil case "gun", "grpc": return "gun", nil + case "hy2", "hysteria2": + return "hysteria2", nil default: return "", newError("Config: unknown transport protocol: ", p) } @@ -302,6 +325,7 @@ type StreamConfig struct { QUICSettings *QUICConfig `json:"quicSettings"` GunSettings *GunConfig `json:"gunSettings"` GRPCSettings *GunConfig `json:"grpcSettings"` + Hy2Settings *Hy2Config `json:"hy2Settings"` SocketSettings *socketcfg.SocketConfig `json:"sockopt"` } @@ -403,6 +427,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) { Settings: serial.ToTypedMessage(gs), }) } + if c.Hy2Settings != nil { + hy2, err := c.Hy2Settings.Build() + if err != nil { + return nil, newError("Failed to build hy2 config.").Base(err) + } + config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{ + ProtocolName: "hysteria2", + Settings: serial.ToTypedMessage(hy2), + }) + } if c.SocketSettings != nil { ss, err := c.SocketSettings.Build() if err != nil { diff --git a/infra/conf/v4/v2ray.go b/infra/conf/v4/v2ray.go index 9ca662d13..4f1190175 100644 --- a/infra/conf/v4/v2ray.go +++ b/infra/conf/v4/v2ray.go @@ -35,6 +35,7 @@ var ( "vless": func() interface{} { return new(VLessInboundConfig) }, "vmess": func() interface{} { return new(VMessInboundConfig) }, "trojan": func() interface{} { return new(TrojanServerConfig) }, + "hysteria2": func() interface{} { return new(Hysteria2ServerConfig) }, }, "protocol", "settings") outboundConfigLoader = loader.NewJSONConfigLoader(loader.ConfigCreatorCache{ @@ -46,6 +47,7 @@ var ( "vless": func() interface{} { return new(VLessOutboundConfig) }, "vmess": func() interface{} { return new(VMessOutboundConfig) }, "trojan": func() interface{} { return new(TrojanClientConfig) }, + "hysteria2": func() interface{} { return new(Hysteria2ClientConfig) }, "dns": func() interface{} { return new(DNSOutboundConfig) }, "loopback": func() interface{} { return new(LoopbackConfig) }, }, "protocol", "settings") diff --git a/main/distro/all/all.go b/main/distro/all/all.go index f81eb8437..ba86a4920 100644 --- a/main/distro/all/all.go +++ b/main/distro/all/all.go @@ -53,6 +53,7 @@ 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/hysteria2" _ "github.com/v2fly/v2ray-core/v5/proxy/shadowsocks2022" // Transports @@ -82,6 +83,8 @@ import ( _ "github.com/v2fly/v2ray-core/v5/transport/internet/httpupgrade" + _ "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" + // Transport headers _ "github.com/v2fly/v2ray-core/v5/transport/internet/headers/http" _ "github.com/v2fly/v2ray-core/v5/transport/internet/headers/noop" diff --git a/proxy/hysteria2/client.go b/proxy/hysteria2/client.go new file mode 100644 index 000000000..4caf6362a --- /dev/null +++ b/proxy/hysteria2/client.go @@ -0,0 +1,216 @@ +package hysteria2 + +import ( + "context" + + hyProtocol "github.com/v2fly/hysteria/core/v2/international/protocol" + + core "github.com/v2fly/v2ray-core/v5" + "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/net/packetaddr" + "github.com/v2fly/v2ray-core/v5/common/protocol" + "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/features/policy" + "github.com/v2fly/v2ray-core/v5/proxy" + "github.com/v2fly/v2ray-core/v5/transport" + "github.com/v2fly/v2ray-core/v5/transport/internet" + hyTransport "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" + "github.com/v2fly/v2ray-core/v5/transport/internet/udp" +) + +// Client is an inbound handler +type Client struct { + serverPicker protocol.ServerPicker + policyManager policy.Manager +} + +// NewClient create a new client. +func NewClient(ctx context.Context, config *ClientConfig) (*Client, error) { + serverList := protocol.NewServerList() + for _, rec := range config.Server { + s, err := protocol.NewServerSpecFromPB(rec) + if err != nil { + return nil, newError("failed to parse server spec").Base(err) + } + serverList.AddServer(s) + } + if serverList.Size() == 0 { + return nil, newError("0 server") + } + + v := core.MustFromContext(ctx) + client := &Client{ + serverPicker: protocol.NewRoundRobinServerPicker(serverList), + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + } + return client, nil +} + +// Process implements OutboundHandler.Process(). +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 server *protocol.ServerSpec + var conn internet.Connection + + err := retry.ExponentialBackoff(5, 100).On(func() error { + server = c.serverPicker.PickServer() + rawConn, err := dialer.Dial(ctx, server.Destination()) + 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 ", server.Destination().NetAddr()).WriteToLog(session.ExportIDToError(ctx)) + + defer conn.Close() + + iConn := conn + if statConn, ok := conn.(*internet.StatCouterConnection); ok { + iConn = statConn.Connection // will not count the UDP traffic. + } + hyConn, IsHy2Transport := iConn.(*hyTransport.HyConn) + + if !IsHy2Transport && network == net.Network_UDP { + // hysteria2 need to use udp extension to proxy UDP. + return newError(hyTransport.CanNotUseUdpExtension) + } + + user := server.PickUser() + userLevel := uint32(0) + if user != nil { + userLevel = user.Level + } + sessionPolicy := c.policyManager.ForLevel(userLevel) + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + + if packetConn, err := packetaddr.ToPacketAddrConn(link, destination); err == nil { + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + var buffer [2048]byte + n, addr, err := packetConn.ReadFrom(buffer[:]) + if err != nil { + return newError("failed to read a packet").Base(err) + } + dest := net.DestinationFromAddr(addr) + + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + connWriter := &ConnWriter{Writer: bufferWriter, Target: dest} + packetWriter := &PacketWriter{Writer: connWriter, Target: dest, HyConn: hyConn} + + // write some request payload to buffer + if _, err := packetWriter.WriteTo(buffer[:n], addr); err != nil { + return newError("failed to write a request payload").Base(err) + } + + // Flush; bufferWriter.WriteMultiBuffer now is bufferWriter.writer.WriteMultiBuffer + if err = bufferWriter.SetBuffered(false); err != nil { + return newError("failed to flush payload").Base(err).AtWarning() + } + + return udp.CopyPacketConn(packetWriter, packetConn, udp.UpdateActivity(timer)) + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + packetReader := &PacketReader{Reader: conn, HyConn: hyConn} + packetConnectionReader := &PacketConnectionReader{reader: packetReader} + + return udp.CopyPacketConn(packetConn, packetConnectionReader, udp.UpdateActivity(timer)) + } + + responseDoneAndCloseWriter := task.OnSuccess(getResponse, task.Close(link.Writer)) + if err := task.Run(ctx, postRequest, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + + return nil + } + + postRequest := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + var bodyWriter buf.Writer + bufferWriter := buf.NewBufferedWriter(buf.NewWriter(conn)) + connWriter := &ConnWriter{Writer: bufferWriter, Target: destination} + bodyWriter = connWriter + + if network == net.Network_UDP { + bodyWriter = &PacketWriter{Writer: connWriter, Target: destination, HyConn: hyConn} + } else { + // write some request payload to buffer + err = buf.CopyOnceTimeout(link.Reader, bodyWriter, proxy.FirstPayloadTimeout) + switch err { + case buf.ErrNotTimeoutReader, buf.ErrReadTimeout: + if err := connWriter.WriteTCPHeader(); err != nil { + return newError("failed to write request header").Base(err).AtWarning() + } + case nil: + default: + return newError("failed to write a request payload").Base(err).AtWarning() + } + // Flush; bufferWriter.WriteMultiBuffer now is bufferWriter.writer.WriteMultiBuffer + if err = bufferWriter.SetBuffered(false); err != nil { + return newError("failed to flush payload").Base(err).AtWarning() + } + } + + if err = buf.Copy(link.Reader, bodyWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transfer request payload").Base(err).AtInfo() + } + + return nil + } + + getResponse := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + var reader buf.Reader + if network == net.Network_UDP { + reader = &PacketReader{ + Reader: conn, HyConn: hyConn, + } + } else { + ok, msg, err := hyProtocol.ReadTCPResponse(conn) + if err != nil { + return err + } + if !ok { + return newError(msg) + } + reader = buf.NewReader(conn) + } + return buf.Copy(reader, link.Writer, buf.UpdateActivity(timer)) + } + + responseDoneAndCloseWriter := task.OnSuccess(getResponse, task.Close(link.Writer)) + if err := task.Run(ctx, postRequest, responseDoneAndCloseWriter); err != nil { + return newError("connection ends").Base(err) + } + + return nil +} + +func init() { + common.Must(common.RegisterConfig((*ClientConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewClient(ctx, config.(*ClientConfig)) + })) +} diff --git a/proxy/hysteria2/config.go b/proxy/hysteria2/config.go new file mode 100644 index 000000000..853d84d65 --- /dev/null +++ b/proxy/hysteria2/config.go @@ -0,0 +1,18 @@ +package hysteria2 + +import ( + "github.com/v2fly/v2ray-core/v5/common/protocol" +) + +// MemoryAccount is an account type converted from Account. +type MemoryAccount struct{} + +// AsAccount implements protocol.AsAccount. +func (a *Account) AsAccount() (protocol.Account, error) { + return &MemoryAccount{}, nil +} + +// Equals implements protocol.Account.Equals(). +func (a *MemoryAccount) Equals(another protocol.Account) bool { + return false +} diff --git a/proxy/hysteria2/config.pb.go b/proxy/hysteria2/config.pb.go new file mode 100644 index 000000000..1d9d291fa --- /dev/null +++ b/proxy/hysteria2/config.pb.go @@ -0,0 +1,282 @@ +package hysteria2 + +import ( + packetaddr "github.com/v2fly/v2ray-core/v5/common/net/packetaddr" + protocol "github.com/v2fly/v2ray-core/v5/common/protocol" + _ "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 Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_hysteria2_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_proxy_hysteria2_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 Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_proxy_hysteria2_config_proto_rawDescGZIP(), []int{0} +} + +type ClientConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Server []*protocol.ServerEndpoint `protobuf:"bytes,1,rep,name=server,proto3" json:"server,omitempty"` +} + +func (x *ClientConfig) Reset() { + *x = ClientConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_hysteria2_config_proto_msgTypes[1] + 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_hysteria2_config_proto_msgTypes[1] + 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_hysteria2_config_proto_rawDescGZIP(), []int{1} +} + +func (x *ClientConfig) GetServer() []*protocol.ServerEndpoint { + if x != nil { + return x.Server + } + return nil +} + +type ServerConfig struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PacketEncoding packetaddr.PacketAddrType `protobuf:"varint,1,opt,name=packet_encoding,json=packetEncoding,proto3,enum=v2ray.core.net.packetaddr.PacketAddrType" json:"packet_encoding,omitempty"` +} + +func (x *ServerConfig) Reset() { + *x = ServerConfig{} + if protoimpl.UnsafeEnabled { + mi := &file_proxy_hysteria2_config_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ServerConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ServerConfig) ProtoMessage() {} + +func (x *ServerConfig) ProtoReflect() protoreflect.Message { + mi := &file_proxy_hysteria2_config_proto_msgTypes[2] + 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 ServerConfig.ProtoReflect.Descriptor instead. +func (*ServerConfig) Descriptor() ([]byte, []int) { + return file_proxy_hysteria2_config_proto_rawDescGZIP(), []int{2} +} + +func (x *ServerConfig) GetPacketEncoding() packetaddr.PacketAddrType { + if x != nil { + return x.PacketEncoding + } + return packetaddr.PacketAddrType(0) +} + +var File_proxy_hysteria2_config_proto protoreflect.FileDescriptor + +var file_proxy_hysteria2_config_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2f, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, + 0x32, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x1a, + 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x2e, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x1a, 0x22, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2f, 0x6e, 0x65, 0x74, 0x2f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x61, 0x64, 0x64, + 0x72, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x21, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2f, + 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x73, 0x70, 0x65, 0x63, 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, 0x09, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x6d, + 0x0a, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x42, + 0x0a, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x63, 0x6f, 0x6c, 0x2e, 0x53, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x06, 0x73, 0x65, 0x72, 0x76, + 0x65, 0x72, 0x3a, 0x19, 0x82, 0xb5, 0x18, 0x15, 0x0a, 0x08, 0x6f, 0x75, 0x74, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x12, 0x09, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x22, 0x7c, 0x0a, + 0x0c, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x52, 0x0a, + 0x0f, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, + 0x6f, 0x72, 0x65, 0x2e, 0x6e, 0x65, 0x74, 0x2e, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x61, 0x64, + 0x64, 0x72, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x41, 0x64, 0x64, 0x72, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x0e, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x74, 0x45, 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, + 0x67, 0x3a, 0x18, 0x82, 0xb5, 0x18, 0x14, 0x0a, 0x07, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, + 0x12, 0x09, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x42, 0x6f, 0x0a, 0x1e, 0x63, + 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x2e, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x50, 0x01, 0x5a, + 0x2e, 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, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0xaa, + 0x02, 0x1a, 0x56, 0x32, 0x52, 0x61, 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x50, 0x72, 0x6f, + 0x78, 0x79, 0x2e, 0x48, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_proxy_hysteria2_config_proto_rawDescOnce sync.Once + file_proxy_hysteria2_config_proto_rawDescData = file_proxy_hysteria2_config_proto_rawDesc +) + +func file_proxy_hysteria2_config_proto_rawDescGZIP() []byte { + file_proxy_hysteria2_config_proto_rawDescOnce.Do(func() { + file_proxy_hysteria2_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_proxy_hysteria2_config_proto_rawDescData) + }) + return file_proxy_hysteria2_config_proto_rawDescData +} + +var file_proxy_hysteria2_config_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_proxy_hysteria2_config_proto_goTypes = []any{ + (*Account)(nil), // 0: v2ray.core.proxy.hysteria2.Account + (*ClientConfig)(nil), // 1: v2ray.core.proxy.hysteria2.ClientConfig + (*ServerConfig)(nil), // 2: v2ray.core.proxy.hysteria2.ServerConfig + (*protocol.ServerEndpoint)(nil), // 3: v2ray.core.common.protocol.ServerEndpoint + (packetaddr.PacketAddrType)(0), // 4: v2ray.core.net.packetaddr.PacketAddrType +} +var file_proxy_hysteria2_config_proto_depIdxs = []int32{ + 3, // 0: v2ray.core.proxy.hysteria2.ClientConfig.server:type_name -> v2ray.core.common.protocol.ServerEndpoint + 4, // 1: v2ray.core.proxy.hysteria2.ServerConfig.packet_encoding:type_name -> v2ray.core.net.packetaddr.PacketAddrType + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proxy_hysteria2_config_proto_init() } +func file_proxy_hysteria2_config_proto_init() { + if File_proxy_hysteria2_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_proxy_hysteria2_config_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_hysteria2_config_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*ClientConfig); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_proxy_hysteria2_config_proto_msgTypes[2].Exporter = func(v any, i int) any { + switch v := v.(*ServerConfig); 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_hysteria2_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proxy_hysteria2_config_proto_goTypes, + DependencyIndexes: file_proxy_hysteria2_config_proto_depIdxs, + MessageInfos: file_proxy_hysteria2_config_proto_msgTypes, + }.Build() + File_proxy_hysteria2_config_proto = out.File + file_proxy_hysteria2_config_proto_rawDesc = nil + file_proxy_hysteria2_config_proto_goTypes = nil + file_proxy_hysteria2_config_proto_depIdxs = nil +} diff --git a/proxy/hysteria2/config.proto b/proxy/hysteria2/config.proto new file mode 100644 index 000000000..ccda75b5e --- /dev/null +++ b/proxy/hysteria2/config.proto @@ -0,0 +1,28 @@ +syntax = "proto3"; + +package v2ray.core.proxy.hysteria2; +option csharp_namespace = "V2Ray.Core.Proxy.Hysteria2"; +option go_package = "github.com/v2fly/v2ray-core/v5/proxy/hysteria2"; +option java_package = "com.v2ray.core.proxy.hysteria2"; +option java_multiple_files = true; + +import "common/net/packetaddr/config.proto"; +import "common/protocol/server_spec.proto"; +import "common/protoext/extensions.proto"; + +message Account { +} + +message ClientConfig { + option (v2ray.core.common.protoext.message_opt).type = "outbound"; + option (v2ray.core.common.protoext.message_opt).short_name = "hysteria2"; + + repeated v2ray.core.common.protocol.ServerEndpoint server = 1; +} + +message ServerConfig { + option (v2ray.core.common.protoext.message_opt).type = "inbound"; + option (v2ray.core.common.protoext.message_opt).short_name = "hysteria2"; + + v2ray.core.net.packetaddr.PacketAddrType packet_encoding = 1; +} diff --git a/proxy/hysteria2/errors.generated.go b/proxy/hysteria2/errors.generated.go new file mode 100644 index 000000000..105317031 --- /dev/null +++ b/proxy/hysteria2/errors.generated.go @@ -0,0 +1,9 @@ +package hysteria2 + +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/hysteria2/hysteria2.go b/proxy/hysteria2/hysteria2.go new file mode 100644 index 000000000..013f7d5cc --- /dev/null +++ b/proxy/hysteria2/hysteria2.go @@ -0,0 +1 @@ +package hysteria2 diff --git a/proxy/hysteria2/protocol.go b/proxy/hysteria2/protocol.go new file mode 100644 index 000000000..6ea7fd335 --- /dev/null +++ b/proxy/hysteria2/protocol.go @@ -0,0 +1,210 @@ +package hysteria2 + +import ( + "io" + "math/rand" + + hyProtocol "github.com/v2fly/hysteria/core/v2/international/protocol" + "github.com/apernet/quic-go/quicvarint" + + "github.com/v2fly/v2ray-core/v5/common/buf" + "github.com/v2fly/v2ray-core/v5/common/net" + hyTransport "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" +) + +const ( + paddingChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +// ConnWriter is TCP Connection Writer Wrapper +type ConnWriter struct { + io.Writer + Target net.Destination + TCPHeaderSent bool +} + +// Write implements io.Writer +func (c *ConnWriter) Write(p []byte) (n int, err error) { + if !c.TCPHeaderSent { + if err := c.writeTCPHeader(); err != nil { + return 0, newError("failed to write request header").Base(err) + } + } + + return c.Writer.Write(p) +} + +// WriteMultiBuffer implements buf.Writer +func (c *ConnWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + defer buf.ReleaseMulti(mb) + + for _, b := range mb { + if !b.IsEmpty() { + if _, err := c.Write(b.Bytes()); err != nil { + return err + } + } + } + + return nil +} + +func (c *ConnWriter) WriteTCPHeader() error { + if !c.TCPHeaderSent { + if err := c.writeTCPHeader(); err != nil { + return err + } + } + return nil +} + +func QuicLen(s int) int { + return int(quicvarint.Len(uint64(s))) +} + +func (c *ConnWriter) writeTCPHeader() error { + c.TCPHeaderSent = true + + paddingLen := 64 + rand.Intn(512-64) + padding := make([]byte, paddingLen) + for i := range padding { + padding[i] = paddingChars[rand.Intn(len(paddingChars))] + } + addressAndPort := c.Target.NetAddr() + addressLen := len(addressAndPort) + if addressLen > hyProtocol.MaxAddressLength { + return newError("address length too large: ", addressLen) + } + size := QuicLen(addressLen) + addressLen + QuicLen(paddingLen) + paddingLen + + buf := make([]byte, size) + i := hyProtocol.VarintPut(buf, uint64(addressLen)) + i += copy(buf[i:], addressAndPort) + i += hyProtocol.VarintPut(buf[i:], uint64(paddingLen)) + copy(buf[i:], padding) + + _, err := c.Writer.Write(buf) + return err +} + +// PacketWriter UDP Connection Writer Wrapper +type PacketWriter struct { + io.Writer + HyConn *hyTransport.HyConn + Target net.Destination +} + +// WriteMultiBuffer implements buf.Writer +func (w *PacketWriter) WriteMultiBuffer(mb buf.MultiBuffer) error { + for _, b := range mb { + if b.IsEmpty() { + continue + } + if _, err := w.writePacket(b.Bytes(), w.Target); err != nil { + buf.ReleaseMulti(mb) + return err + } + } + + return nil +} + +// WriteMultiBufferWithMetadata writes udp packet with destination specified +func (w *PacketWriter) WriteMultiBufferWithMetadata(mb buf.MultiBuffer, dest net.Destination) error { + for _, b := range mb { + if b.IsEmpty() { + continue + } + if _, err := w.writePacket(b.Bytes(), dest); err != nil { + buf.ReleaseMulti(mb) + return err + } + } + + return nil +} + +func (w *PacketWriter) WriteTo(payload []byte, addr net.Addr) (int, error) { + dest := net.DestinationFromAddr(addr) + + return w.writePacket(payload, dest) +} + +func (w *PacketWriter) writePacket(payload []byte, dest net.Destination) (int, error) { + return w.HyConn.WritePacket(payload, dest) +} + +// ConnReader is TCP Connection Reader Wrapper +type ConnReader struct { + io.Reader + Target net.Destination +} + +// Read implements io.Reader +func (c *ConnReader) Read(p []byte) (int, error) { + return c.Reader.Read(p) +} + +// ReadMultiBuffer implements buf.Reader +func (c *ConnReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + b := buf.New() + _, err := b.ReadFrom(c) + if err != nil { + return nil, err + } + return buf.MultiBuffer{b}, nil +} + +// PacketPayload combines udp payload and destination +type PacketPayload struct { + Target net.Destination + Buffer buf.MultiBuffer +} + +// PacketReader is UDP Connection Reader Wrapper +type PacketReader struct { + io.Reader + HyConn *hyTransport.HyConn +} + +// ReadMultiBuffer implements buf.Reader +func (r *PacketReader) ReadMultiBuffer() (buf.MultiBuffer, error) { + p, err := r.ReadMultiBufferWithMetadata() + if p != nil { + return p.Buffer, err + } + return nil, err +} + +// ReadMultiBufferWithMetadata reads udp packet with destination +func (r *PacketReader) ReadMultiBufferWithMetadata() (*PacketPayload, error) { + _, data, dest, err := r.HyConn.ReadPacket() + if err != nil { + return nil, err + } + b := buf.FromBytes(data) + return &PacketPayload{Target: *dest, Buffer: buf.MultiBuffer{b}}, nil +} + +type PacketConnectionReader struct { + reader *PacketReader + payload *PacketPayload +} + +func (r *PacketConnectionReader) ReadFrom(p []byte) (n int, addr net.Addr, err error) { + if r.payload == nil || r.payload.Buffer.IsEmpty() { + r.payload, err = r.reader.ReadMultiBufferWithMetadata() + if err != nil { + return + } + } + + addr = &net.UDPAddr{ + IP: r.payload.Target.Address.IP(), + Port: int(r.payload.Target.Port), + } + + r.payload.Buffer, n = buf.SplitFirstBytes(r.payload.Buffer, p) + + return +} diff --git a/proxy/hysteria2/server.go b/proxy/hysteria2/server.go new file mode 100644 index 000000000..ec5ffddcb --- /dev/null +++ b/proxy/hysteria2/server.go @@ -0,0 +1,216 @@ +package hysteria2 + +import ( + "context" + "io" + "time" + + hyProtocol "github.com/v2fly/hysteria/core/v2/international/protocol" + + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/buf" + "github.com/v2fly/v2ray-core/v5/common/errors" + "github.com/v2fly/v2ray-core/v5/common/log" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/net/packetaddr" + udp_proto "github.com/v2fly/v2ray-core/v5/common/protocol/udp" + "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/features/policy" + "github.com/v2fly/v2ray-core/v5/features/routing" + "github.com/v2fly/v2ray-core/v5/transport/internet" + hyTransport "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" + "github.com/v2fly/v2ray-core/v5/transport/internet/udp" +) + +func init() { + common.Must(common.RegisterConfig((*ServerConfig)(nil), func(ctx context.Context, config interface{}) (interface{}, error) { + return NewServer(ctx, config.(*ServerConfig)) + })) +} + +// Server is an inbound connection handler that handles messages in protocol. +type Server struct { + policyManager policy.Manager + packetEncoding packetaddr.PacketAddrType +} + +// NewServer creates a new inbound handler. +func NewServer(ctx context.Context, config *ServerConfig) (*Server, error) { + v := core.MustFromContext(ctx) + server := &Server{ + policyManager: v.GetFeature(policy.ManagerType()).(policy.Manager), + packetEncoding: config.PacketEncoding, + } + return server, nil +} + +// Network implements proxy.Inbound.Network(). +func (s *Server) Network() []net.Network { + return []net.Network{net.Network_TCP, net.Network_UNIX} +} + +// Process implements proxy.Inbound.Process(). +func (s *Server) Process(ctx context.Context, network net.Network, conn internet.Connection, dispatcher routing.Dispatcher) error { + sid := session.ExportIDToError(ctx) + + iConn := conn + if statConn, ok := conn.(*internet.StatCouterConnection); ok { + iConn = statConn.Connection // will not count the UDP traffic. + } + hyConn, IsHy2Transport := iConn.(*hyTransport.HyConn) + + if IsHy2Transport && hyConn.IsUDPExtension { + network = net.Network_UDP + } + + if !IsHy2Transport && network == net.Network_UDP { + return newError(hyTransport.CanNotUseUdpExtension) + } + + sessionPolicy := s.policyManager.ForLevel(0) + if err := conn.SetReadDeadline(time.Now().Add(sessionPolicy.Timeouts.Handshake)); err != nil { + return newError("unable to set read deadline").Base(err).AtWarning() + } + + bufferedReader := &buf.BufferedReader{ + Reader: buf.NewReader(conn), + } + clientReader := &ConnReader{Reader: bufferedReader} + + if err := conn.SetReadDeadline(time.Time{}); err != nil { + return newError("unable to set read deadline").Base(err).AtWarning() + } + + if network == net.Network_UDP { // handle udp request + return s.handleUDPPayload(ctx, + &PacketReader{Reader: clientReader, HyConn: hyConn}, + &PacketWriter{Writer: conn, HyConn: hyConn}, dispatcher) + } + + var reqAddr string + var err error + reqAddr, err = hyProtocol.ReadTCPRequest(conn) + if err != nil { + return newError("failed to parse header").Base(err) + } + err = hyProtocol.WriteTCPResponse(conn, true, "") + if err != nil { + return newError("failed to send response").Base(err) + } + + address, stringPort, err := net.SplitHostPort(reqAddr) + if err != nil { + return err + } + port, err := net.PortFromString(stringPort) + if err != nil { + return err + } + destination := net.Destination{Network: network, Address: net.ParseAddress(address), Port: port} + + inbound := session.InboundFromContext(ctx) + if inbound == nil { + panic("no inbound metadata") + } + sessionPolicy = s.policyManager.ForLevel(0) + + ctx = log.ContextWithAccessMessage(ctx, &log.AccessMessage{ + From: conn.RemoteAddr(), + To: destination, + Status: log.AccessAccepted, + Reason: "", + }) + + newError("received request for ", destination).WriteToLog(sid) + return s.handleConnection(ctx, sessionPolicy, destination, clientReader, buf.NewWriter(conn), dispatcher) +} + +func (s *Server) handleConnection(ctx context.Context, sessionPolicy policy.Session, + destination net.Destination, + clientReader buf.Reader, + clientWriter buf.Writer, dispatcher routing.Dispatcher, +) error { + ctx, cancel := context.WithCancel(ctx) + timer := signal.CancelAfterInactivity(ctx, cancel, sessionPolicy.Timeouts.ConnectionIdle) + ctx = policy.ContextWithBufferPolicy(ctx, sessionPolicy.Buffer) + + link, err := dispatcher.Dispatch(ctx, destination) + if err != nil { + return newError("failed to dispatch request to ", destination).Base(err) + } + + requestDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.DownlinkOnly) + + if err := buf.Copy(clientReader, link.Writer, buf.UpdateActivity(timer)); err != nil { + return newError("failed to transfer request").Base(err) + } + return nil + } + + responseDone := func() error { + defer timer.SetTimeout(sessionPolicy.Timeouts.UplinkOnly) + + if err := buf.Copy(link.Reader, clientWriter, buf.UpdateActivity(timer)); err != nil { + return newError("failed to write response").Base(err) + } + return nil + } + + requestDonePost := task.OnSuccess(requestDone, task.Close(link.Writer)) + if err := task.Run(ctx, requestDonePost, responseDone); err != nil { + common.Must(common.Interrupt(link.Reader)) + common.Must(common.Interrupt(link.Writer)) + return newError("connection ends").Base(err) + } + + return nil +} + +func (s *Server) handleUDPPayload(ctx context.Context, clientReader *PacketReader, clientWriter *PacketWriter, dispatcher routing.Dispatcher) error { + udpDispatcherConstructor := udp.NewSplitDispatcher + switch s.packetEncoding { + case packetaddr.PacketAddrType_None: + case packetaddr.PacketAddrType_Packet: + packetAddrDispatcherFactory := udp.NewPacketAddrDispatcherCreator(ctx) + udpDispatcherConstructor = packetAddrDispatcherFactory.NewPacketAddrDispatcher + } + + udpServer := udpDispatcherConstructor(dispatcher, func(ctx context.Context, packet *udp_proto.Packet) { + if err := clientWriter.WriteMultiBufferWithMetadata(buf.MultiBuffer{packet.Payload}, packet.Source); err != nil { + newError("failed to write response").Base(err).AtWarning().WriteToLog(session.ExportIDToError(ctx)) + } + }) + + inbound := session.InboundFromContext(ctx) + + for { + select { + case <-ctx.Done(): + return nil + default: + p, err := clientReader.ReadMultiBufferWithMetadata() + if err != nil { + if errors.Cause(err) != io.EOF { + return newError("unexpected EOF").Base(err) + } + return nil + } + currentPacketCtx := ctx + currentPacketCtx = log.ContextWithAccessMessage(currentPacketCtx, &log.AccessMessage{ + From: inbound.Source, + To: p.Target, + Status: log.AccessAccepted, + Reason: "", + }) + newError("tunnelling request to ", p.Target).WriteToLog(session.ExportIDToError(ctx)) + + for _, b := range p.Buffer { + udpServer.Dispatch(currentPacketCtx, p.Target, b) + } + } + } +} diff --git a/testing/scenarios/hy2_test.go b/testing/scenarios/hy2_test.go new file mode 100644 index 000000000..0a2a92987 --- /dev/null +++ b/testing/scenarios/hy2_test.go @@ -0,0 +1,468 @@ +package scenarios + +import ( + "testing" + "time" + + "golang.org/x/sync/errgroup" + "google.golang.org/protobuf/types/known/anypb" + + core "github.com/v2fly/v2ray-core/v5" + "github.com/v2fly/v2ray-core/v5/app/log" + "github.com/v2fly/v2ray-core/v5/app/proxyman" + "github.com/v2fly/v2ray-core/v5/common" + clog "github.com/v2fly/v2ray-core/v5/common/log" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/protocol" + "github.com/v2fly/v2ray-core/v5/common/protocol/tls/cert" + "github.com/v2fly/v2ray-core/v5/common/serial" + "github.com/v2fly/v2ray-core/v5/common/uuid" + "github.com/v2fly/v2ray-core/v5/proxy/dokodemo" + "github.com/v2fly/v2ray-core/v5/proxy/freedom" + "github.com/v2fly/v2ray-core/v5/proxy/hysteria2" + "github.com/v2fly/v2ray-core/v5/proxy/vmess" + "github.com/v2fly/v2ray-core/v5/proxy/vmess/inbound" + "github.com/v2fly/v2ray-core/v5/proxy/vmess/outbound" + "github.com/v2fly/v2ray-core/v5/testing/servers/tcp" + "github.com/v2fly/v2ray-core/v5/testing/servers/udp" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/headers/http" + hyTransport "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" + tcpTransport "github.com/v2fly/v2ray-core/v5/transport/internet/tcp" + "github.com/v2fly/v2ray-core/v5/transport/internet/tls" +) + +func TestVMessHysteria2Congestion(t *testing.T) { + for _, v := range []string{"bbr", "brutal"} { + testVMessHysteria2(t, v) + } +} + +func testVMessHysteria2(t *testing.T, congestionType string) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + userID := protocol.NewID(uuid.New()) + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*anypb.Any{ + serial.ToTypedMessage(&log.Config{ + Error: &log.LogSpecification{Level: clog.Severity_Debug, Type: log.LogType_Console}, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + + StreamSettings: &internet.StreamConfig{ + ProtocolName: "hysteria2", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*anypb.Any{ + serial.ToTypedMessage( + &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }, + ), + }, + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "hysteria2", + Settings: serial.ToTypedMessage(&hyTransport.Config{ + Congestion: &hyTransport.Congestion{Type: congestionType, UpMbps: 100, DownMbps: 100}, + Password: "password", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&inbound.Config{ + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 0, + }), + }, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*anypb.Any{ + serial.ToTypedMessage(&log.Config{ + Error: &log.LogSpecification{Level: clog.Severity_Debug, Type: log.LogType_Console}, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "hysteria2", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*anypb.Any{ + serial.ToTypedMessage( + &tls.Config{ + ServerName: "www.v2fly.org", + AllowInsecure: true, + }, + ), + }, + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "hysteria2", + Settings: serial.ToTypedMessage(&hyTransport.Config{ + Congestion: &hyTransport.Congestion{Type: congestionType, UpMbps: 100, DownMbps: 100}, + Password: "password", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&outbound.Config{ + Receiver: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&vmess.Account{ + Id: userID.String(), + AlterId: 0, + SecuritySettings: &protocol.SecurityConfig{ + Type: protocol.SecurityType_NONE, + }, + }), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestHysteria2Offical(t *testing.T) { + for _, v := range []bool{true, false} { + testHysteria2Offical(t, v) + } +} + +func testHysteria2Offical(t *testing.T, isUDP bool) { + var dest net.Destination + var err error + if isUDP { + udpServer := udp.Server{ + MsgProcessor: xor, + } + dest, err = udpServer.Start() + common.Must(err) + defer udpServer.Close() + } else { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err = tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + } + + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*anypb.Any{ + serial.ToTypedMessage(&log.Config{ + Error: &log.LogSpecification{Level: clog.Severity_Debug, Type: log.LogType_Console}, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + ProtocolName: "hysteria2", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*anypb.Any{ + serial.ToTypedMessage( + &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }, + ), + }, + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "hysteria2", + Settings: serial.ToTypedMessage(&hyTransport.Config{ + Congestion: &hyTransport.Congestion{Type: "brutal", UpMbps: 100, DownMbps: 100}, + UseUdpExtension: true, + Password: "password", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&hysteria2.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*anypb.Any{ + serial.ToTypedMessage(&log.Config{ + Error: &log.LogSpecification{Level: clog.Severity_Debug, Type: log.LogType_Console}, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP, net.Network_UDP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + ProtocolName: "hysteria2", + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*anypb.Any{ + serial.ToTypedMessage( + &tls.Config{ + ServerName: "www.v2fly.org", + AllowInsecure: true, + }, + ), + }, + TransportSettings: []*internet.TransportConfig{ + { + ProtocolName: "hysteria2", + Settings: serial.ToTypedMessage(&hyTransport.Config{ + Congestion: &hyTransport.Congestion{Type: "brutal", UpMbps: 100, DownMbps: 100}, + UseUdpExtension: true, + Password: "password", + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&hysteria2.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&hysteria2.Account{}), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 10; i++ { + if isUDP { + errg.Go(testUDPConn(clientPort, 1500, time.Second*4)) + } else { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} + +func TestHysteria2OnTCP(t *testing.T) { + tcpServer := tcp.Server{ + MsgProcessor: xor, + } + dest, err := tcpServer.Start() + common.Must(err) + defer tcpServer.Close() + + serverPort := udp.PickPort() + serverConfig := &core.Config{ + App: []*anypb.Any{ + serial.ToTypedMessage(&log.Config{ + Error: &log.LogSpecification{Level: clog.Severity_Debug, Type: log.LogType_Console}, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(serverPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*anypb.Any{ + serial.ToTypedMessage( + &tls.Config{ + Certificate: []*tls.Certificate{tls.ParseCertificate(cert.MustGenerate(nil))}, + }, + ), + }, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_TCP, + Settings: serial.ToTypedMessage(&tcpTransport.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{}), + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&hysteria2.ServerConfig{}), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + ProxySettings: serial.ToTypedMessage(&freedom.Config{}), + }, + }, + } + + clientPort := tcp.PickPort() + clientConfig := &core.Config{ + App: []*anypb.Any{ + serial.ToTypedMessage(&log.Config{ + Error: &log.LogSpecification{Level: clog.Severity_Debug, Type: log.LogType_Console}, + }), + }, + Inbound: []*core.InboundHandlerConfig{ + { + ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{ + PortRange: net.SinglePortRange(clientPort), + Listen: net.NewIPOrDomain(net.LocalHostIP), + }), + ProxySettings: serial.ToTypedMessage(&dokodemo.Config{ + Address: net.NewIPOrDomain(dest.Address), + Port: uint32(dest.Port), + NetworkList: &net.NetworkList{ + Network: []net.Network{net.Network_TCP}, + }, + }), + }, + }, + Outbound: []*core.OutboundHandlerConfig{ + { + SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{ + StreamSettings: &internet.StreamConfig{ + SecurityType: serial.GetMessageType(&tls.Config{}), + SecuritySettings: []*anypb.Any{ + serial.ToTypedMessage( + &tls.Config{ + ServerName: "www.v2fly.org", + AllowInsecure: true, + }, + ), + }, + TransportSettings: []*internet.TransportConfig{ + { + Protocol: internet.TransportProtocol_TCP, + Settings: serial.ToTypedMessage(&tcpTransport.Config{ + HeaderSettings: serial.ToTypedMessage(&http.Config{}), + }), + }, + }, + }, + }), + ProxySettings: serial.ToTypedMessage(&hysteria2.ClientConfig{ + Server: []*protocol.ServerEndpoint{ + { + Address: net.NewIPOrDomain(net.LocalHostIP), + Port: uint32(serverPort), + User: []*protocol.User{ + { + Account: serial.ToTypedMessage(&hysteria2.Account{}), + }, + }, + }, + }, + }), + }, + }, + } + + servers, err := InitializeServerConfigs(serverConfig, clientConfig) + if err != nil { + t.Fatal("Failed to initialize all servers: ", err.Error()) + } + defer CloseAllServers(servers) + + var errg errgroup.Group + for i := 0; i < 1; i++ { + errg.Go(testTCPConn(clientPort, 10240*1024, time.Second*40)) + } + + if err := errg.Wait(); err != nil { + t.Error(err) + } +} diff --git a/transport/internet/hysteria2/config.pb.go b/transport/internet/hysteria2/config.pb.go new file mode 100644 index 000000000..f42e82e1b --- /dev/null +++ b/transport/internet/hysteria2/config.pb.go @@ -0,0 +1,271 @@ +package hysteria2 + +import ( + _ "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 Congestion struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + UpMbps uint64 `protobuf:"varint,2,opt,name=up_mbps,json=upMbps,proto3" json:"up_mbps,omitempty"` + DownMbps uint64 `protobuf:"varint,3,opt,name=down_mbps,json=downMbps,proto3" json:"down_mbps,omitempty"` +} + +func (x *Congestion) Reset() { + *x = Congestion{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_hysteria2_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Congestion) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Congestion) ProtoMessage() {} + +func (x *Congestion) ProtoReflect() protoreflect.Message { + mi := &file_transport_internet_hysteria2_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 Congestion.ProtoReflect.Descriptor instead. +func (*Congestion) Descriptor() ([]byte, []int) { + return file_transport_internet_hysteria2_config_proto_rawDescGZIP(), []int{0} +} + +func (x *Congestion) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Congestion) GetUpMbps() uint64 { + if x != nil { + return x.UpMbps + } + return 0 +} + +func (x *Congestion) GetDownMbps() uint64 { + if x != nil { + return x.DownMbps + } + return 0 +} + +type Config struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"` + Congestion *Congestion `protobuf:"bytes,4,opt,name=congestion,proto3" json:"congestion,omitempty"` + IgnoreClientBandwidth bool `protobuf:"varint,5,opt,name=ignore_client_bandwidth,json=ignoreClientBandwidth,proto3" json:"ignore_client_bandwidth,omitempty"` + UseUdpExtension bool `protobuf:"varint,6,opt,name=use_udp_extension,json=useUdpExtension,proto3" json:"use_udp_extension,omitempty"` +} + +func (x *Config) Reset() { + *x = Config{} + if protoimpl.UnsafeEnabled { + mi := &file_transport_internet_hysteria2_config_proto_msgTypes[1] + 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_hysteria2_config_proto_msgTypes[1] + 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 Config.ProtoReflect.Descriptor instead. +func (*Config) Descriptor() ([]byte, []int) { + return file_transport_internet_hysteria2_config_proto_rawDescGZIP(), []int{1} +} + +func (x *Config) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *Config) GetCongestion() *Congestion { + if x != nil { + return x.Congestion + } + return nil +} + +func (x *Config) GetIgnoreClientBandwidth() bool { + if x != nil { + return x.IgnoreClientBandwidth + } + return false +} + +func (x *Config) GetUseUdpExtension() bool { + if x != nil { + return x.UseUdpExtension + } + return false +} + +var File_transport_internet_hysteria2_config_proto protoreflect.FileDescriptor + +var file_transport_internet_hysteria2_config_proto_rawDesc = []byte{ + 0x0a, 0x29, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x2f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x76, 0x32, 0x72, + 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x79, 0x73, 0x74, 0x65, + 0x72, 0x69, 0x61, 0x32, 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, 0x56, 0x0a, 0x0a, 0x43, 0x6f, 0x6e, 0x67, 0x65, 0x73, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x70, 0x5f, 0x6d, + 0x62, 0x70, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x75, 0x70, 0x4d, 0x62, 0x70, + 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x6f, 0x77, 0x6e, 0x5f, 0x6d, 0x62, 0x70, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x64, 0x6f, 0x77, 0x6e, 0x4d, 0x62, 0x70, 0x73, 0x22, 0xf9, + 0x01, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x53, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x76, 0x32, 0x72, 0x61, + 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, + 0x69, 0x61, 0x32, 0x2e, 0x43, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, + 0x63, 0x6f, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x36, 0x0a, 0x17, 0x69, 0x67, + 0x6e, 0x6f, 0x72, 0x65, 0x5f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x62, 0x61, 0x6e, 0x64, + 0x77, 0x69, 0x64, 0x74, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x69, 0x67, 0x6e, + 0x6f, 0x72, 0x65, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x42, 0x61, 0x6e, 0x64, 0x77, 0x69, 0x64, + 0x74, 0x68, 0x12, 0x2a, 0x0a, 0x11, 0x75, 0x73, 0x65, 0x5f, 0x75, 0x64, 0x70, 0x5f, 0x65, 0x78, + 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0f, 0x75, + 0x73, 0x65, 0x55, 0x64, 0x70, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f, 0x6e, 0x3a, 0x1a, + 0x82, 0xb5, 0x18, 0x16, 0x0a, 0x09, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x12, + 0x09, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x42, 0x96, 0x01, 0x0a, 0x2b, 0x63, + 0x6f, 0x6d, 0x2e, 0x76, 0x32, 0x72, 0x61, 0x79, 0x2e, 0x63, 0x6f, 0x72, 0x65, 0x2e, 0x74, 0x72, + 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, + 0x2e, 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0x50, 0x01, 0x5a, 0x3b, 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, 0x74, 0x72, 0x61, + 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, + 0x68, 0x79, 0x73, 0x74, 0x65, 0x72, 0x69, 0x61, 0x32, 0xaa, 0x02, 0x27, 0x56, 0x32, 0x52, 0x61, + 0x79, 0x2e, 0x43, 0x6f, 0x72, 0x65, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, + 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x48, 0x79, 0x73, 0x74, 0x65, 0x72, + 0x69, 0x61, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_transport_internet_hysteria2_config_proto_rawDescOnce sync.Once + file_transport_internet_hysteria2_config_proto_rawDescData = file_transport_internet_hysteria2_config_proto_rawDesc +) + +func file_transport_internet_hysteria2_config_proto_rawDescGZIP() []byte { + file_transport_internet_hysteria2_config_proto_rawDescOnce.Do(func() { + file_transport_internet_hysteria2_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_transport_internet_hysteria2_config_proto_rawDescData) + }) + return file_transport_internet_hysteria2_config_proto_rawDescData +} + +var file_transport_internet_hysteria2_config_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_transport_internet_hysteria2_config_proto_goTypes = []any{ + (*Congestion)(nil), // 0: v2ray.core.transport.internet.hysteria2.Congestion + (*Config)(nil), // 1: v2ray.core.transport.internet.hysteria2.Config +} +var file_transport_internet_hysteria2_config_proto_depIdxs = []int32{ + 0, // 0: v2ray.core.transport.internet.hysteria2.Config.congestion:type_name -> v2ray.core.transport.internet.hysteria2.Congestion + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_transport_internet_hysteria2_config_proto_init() } +func file_transport_internet_hysteria2_config_proto_init() { + if File_transport_internet_hysteria2_config_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_transport_internet_hysteria2_config_proto_msgTypes[0].Exporter = func(v any, i int) any { + switch v := v.(*Congestion); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_transport_internet_hysteria2_config_proto_msgTypes[1].Exporter = func(v any, i int) any { + switch v := v.(*Config); 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_internet_hysteria2_config_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_transport_internet_hysteria2_config_proto_goTypes, + DependencyIndexes: file_transport_internet_hysteria2_config_proto_depIdxs, + MessageInfos: file_transport_internet_hysteria2_config_proto_msgTypes, + }.Build() + File_transport_internet_hysteria2_config_proto = out.File + file_transport_internet_hysteria2_config_proto_rawDesc = nil + file_transport_internet_hysteria2_config_proto_goTypes = nil + file_transport_internet_hysteria2_config_proto_depIdxs = nil +} diff --git a/transport/internet/hysteria2/config.proto b/transport/internet/hysteria2/config.proto new file mode 100644 index 000000000..2796c3f61 --- /dev/null +++ b/transport/internet/hysteria2/config.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package v2ray.core.transport.internet.hysteria2; +option csharp_namespace = "V2Ray.Core.Transport.Internet.Hysteria2"; +option go_package = "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2"; +option java_package = "com.v2ray.core.transport.internet.hysteria2"; +option java_multiple_files = true; + +import "common/protoext/extensions.proto"; + +message Congestion{ + string type = 1; + + uint64 up_mbps = 2; + uint64 down_mbps = 3; +} + +message Config { + option (v2ray.core.common.protoext.message_opt).type = "transport"; + option (v2ray.core.common.protoext.message_opt).short_name = "hysteria2"; + + string password = 3; + Congestion congestion = 4; + bool ignore_client_bandwidth = 5; + bool use_udp_extension = 6; +} diff --git a/transport/internet/hysteria2/conn.go b/transport/internet/hysteria2/conn.go new file mode 100644 index 000000000..ac27de0c0 --- /dev/null +++ b/transport/internet/hysteria2/conn.go @@ -0,0 +1,130 @@ +package hysteria2 + +import ( + "time" + + hyClient "github.com/v2fly/hysteria/core/v2/client" + "github.com/v2fly/hysteria/core/v2/international/protocol" + hyServer "github.com/v2fly/hysteria/core/v2/server" + "github.com/apernet/quic-go" + + "github.com/v2fly/v2ray-core/v5/common/net" +) + +const CanNotUseUdpExtension = "Only hysteria2 proxy protocol can use udpExtension." +const Hy2MustNeedTLS = "Hysteria2 based on QUIC that requires TLS." + +type HyConn struct { + IsUDPExtension bool + IsServer bool + ClientUDPSession hyClient.HyUDPConn + ServerUDPSession *hyServer.UdpSessionEntry + + stream quic.Stream + local net.Addr + remote net.Addr +} + +func (c *HyConn) Read(b []byte) (int, error) { + if c.IsUDPExtension { + n, data, _, err := c.ReadPacket() + copy(b, data) + return n, err + } + return c.stream.Read(b) +} + +func (c *HyConn) Write(b []byte) (int, error) { + if c.IsUDPExtension { + dest, _ := net.ParseDestination("udp:v2fly.org:6666") + return c.WritePacket(b, dest) + } + return c.stream.Write(b) +} + +func (c *HyConn) WritePacket(b []byte, dest net.Destination) (int, error) { + if !c.IsUDPExtension { + return 0, newError(CanNotUseUdpExtension) + } + + if c.IsServer { + msg := &protocol.UDPMessage{ + SessionID: c.ServerUDPSession.ID, + PacketID: 0, + FragID: 0, + FragCount: 1, + Addr: dest.NetAddr(), + Data: b, + } + c.ServerUDPSession.SendCh <- msg + return len(b), nil + } + return len(b), c.ClientUDPSession.Send(b, dest.NetAddr()) +} + +func (c *HyConn) ReadPacket() (int, []byte, *net.Destination, error) { + if !c.IsUDPExtension { + return 0, nil, nil, newError(CanNotUseUdpExtension) + } + + if c.IsServer { + msg, ok := <-c.ServerUDPSession.ReceiveCh + if !ok { + return 0, nil, nil, newError("UDP session receive channel closed") + } + dest, err := net.ParseDestination("udp:" + msg.Addr) + return len(msg.Data), msg.Data, &dest, err + } + data, address, err := c.ClientUDPSession.Receive() + if err != nil { + return 0, nil, nil, err + } + dest, err := net.ParseDestination("udp:" + address) + if err != nil { + return 0, nil, nil, err + } + return len(data), data, &dest, nil +} + +func (c *HyConn) Close() error { + if c.IsUDPExtension { + if !c.IsServer && c.ClientUDPSession == nil || (c.IsServer && c.ServerUDPSession == nil) { + return newError(CanNotUseUdpExtension) + } + if c.IsServer { + c.ServerUDPSession.CloseWithErr(nil) + return nil + } + return c.ClientUDPSession.Close() + } + return c.stream.Close() +} + +func (c *HyConn) LocalAddr() net.Addr { + return c.local +} + +func (c *HyConn) RemoteAddr() net.Addr { + return c.remote +} + +func (c *HyConn) SetDeadline(t time.Time) error { + if c.IsUDPExtension { + return nil + } + return c.stream.SetDeadline(t) +} + +func (c *HyConn) SetReadDeadline(t time.Time) error { + if c.IsUDPExtension { + return nil + } + return c.stream.SetReadDeadline(t) +} + +func (c *HyConn) SetWriteDeadline(t time.Time) error { + if c.IsUDPExtension { + return nil + } + return c.stream.SetWriteDeadline(t) +} diff --git a/transport/internet/hysteria2/dialer.go b/transport/internet/hysteria2/dialer.go new file mode 100644 index 000000000..ca8a708eb --- /dev/null +++ b/transport/internet/hysteria2/dialer.go @@ -0,0 +1,207 @@ +package hysteria2 + +import ( + "context" + "sync" + + hyClient "github.com/v2fly/hysteria/core/v2/client" + hyProtocol "github.com/v2fly/hysteria/core/v2/international/protocol" + "github.com/apernet/quic-go/quicvarint" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/common/session" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/tls" +) + +type dialerConf struct { + net.Destination + *internet.MemoryStreamConfig +} + +var RunningClient map[dialerConf](hyClient.Client) +var ClientMutex sync.Mutex +var MBps uint64 = 1000000 / 8 // MByte + +func GetClientTLSConfig(dest net.Destination, streamSettings *internet.MemoryStreamConfig) (*hyClient.TLSConfig, error) { + config := tls.ConfigFromStreamSettings(streamSettings) + if config == nil { + return nil, newError(Hy2MustNeedTLS) + } + tlsConfig := config.GetTLSConfig(tls.WithDestination(dest)) + + return &hyClient.TLSConfig{ + RootCAs: tlsConfig.RootCAs, + ServerName: tlsConfig.ServerName, + InsecureSkipVerify: tlsConfig.InsecureSkipVerify, + VerifyPeerCertificate: tlsConfig.VerifyPeerCertificate, + }, nil +} + +func ResolveAddress(dest net.Destination) (net.Addr, error) { + var destAddr *net.UDPAddr + if dest.Address.Family().IsIP() { + destAddr = &net.UDPAddr{ + IP: dest.Address.IP(), + Port: int(dest.Port), + } + } else { + addr, err := net.ResolveUDPAddr("udp", dest.NetAddr()) + if err != nil { + return nil, err + } + destAddr = addr + } + return destAddr, nil +} + +type connFactory struct { + hyClient.ConnFactory + + NewFunc func(addr net.Addr) (net.PacketConn, error) +} + +func (f *connFactory) New(addr net.Addr) (net.PacketConn, error) { + return f.NewFunc(addr) +} + +func NewHyClient(dest net.Destination, streamSettings *internet.MemoryStreamConfig) (hyClient.Client, error) { + tlsConfig, err := GetClientTLSConfig(dest, streamSettings) + if err != nil { + return nil, err + } + + serverAddr, err := ResolveAddress(dest) + if err != nil { + return nil, err + } + + config := streamSettings.ProtocolSettings.(*Config) + client, _, err := hyClient.NewClient(&hyClient.Config{ + Auth: config.GetPassword(), + TLSConfig: *tlsConfig, + ServerAddr: serverAddr, + ConnFactory: &connFactory{ + NewFunc: func(addr net.Addr) (net.PacketConn, error) { + rawConn, err := internet.ListenSystemPacket(context.Background(), &net.UDPAddr{ + IP: []byte{0, 0, 0, 0}, + Port: 0, + }, streamSettings.SocketSettings) + if err != nil { + return nil, err + } + return rawConn.(*net.UDPConn), nil + }, + }, + BandwidthConfig: hyClient.BandwidthConfig{MaxTx: config.Congestion.GetUpMbps() * MBps, MaxRx: config.GetCongestion().GetDownMbps() * MBps}, + }) + if err != nil { + return nil, err + } + + return client, nil +} + +func CloseHyClient(dest net.Destination, streamSettings *internet.MemoryStreamConfig) error { + ClientMutex.Lock() + defer ClientMutex.Unlock() + + client, found := RunningClient[dialerConf{dest, streamSettings}] + if found { + delete(RunningClient, dialerConf{dest, streamSettings}) + return client.Close() + } + return nil +} + +func GetHyClient(dest net.Destination, streamSettings *internet.MemoryStreamConfig) (hyClient.Client, error) { + var err error + var client hyClient.Client + + ClientMutex.Lock() + client, found := RunningClient[dialerConf{dest, streamSettings}] + ClientMutex.Unlock() + if !found || !CheckHyClientHealthy(client) { + if found { + // retry + CloseHyClient(dest, streamSettings) + } + client, err = NewHyClient(dest, streamSettings) + if err != nil { + return nil, err + } + ClientMutex.Lock() + RunningClient[dialerConf{dest, streamSettings}] = client + ClientMutex.Unlock() + } + return client, nil +} + +func CheckHyClientHealthy(client hyClient.Client) bool { + quicConn := client.GetQuicConn() + if quicConn == nil { + return false + } + select { + case <-quicConn.Context().Done(): + return false + default: + } + return true +} + +func Dial(ctx context.Context, dest net.Destination, streamSettings *internet.MemoryStreamConfig) (internet.Connection, error) { + config := streamSettings.ProtocolSettings.(*Config) + + client, err := GetHyClient(dest, streamSettings) + if err != nil { + CloseHyClient(dest, streamSettings) + return nil, err + } + + quicConn := client.GetQuicConn() + conn := &HyConn{ + local: quicConn.LocalAddr(), + remote: quicConn.RemoteAddr(), + } + + outbound := session.OutboundFromContext(ctx) + network := net.Network_TCP + if outbound != nil { + network = outbound.Target.Network + } + + if network == net.Network_UDP && config.GetUseUdpExtension() { // only hysteria2 can use udpExtension + conn.IsUDPExtension = true + conn.IsServer = false + conn.ClientUDPSession, err = client.UDP() + if err != nil { + CloseHyClient(dest, streamSettings) + return nil, err + } + return conn, nil + } + + conn.stream, err = client.OpenStream() + if err != nil { + CloseHyClient(dest, streamSettings) + return nil, err + } + + // write TCP frame type + frameSize := int(quicvarint.Len(hyProtocol.FrameTypeTCPRequest)) + buf := make([]byte, frameSize) + hyProtocol.VarintPut(buf, hyProtocol.FrameTypeTCPRequest) + _, err = conn.stream.Write(buf) + if err != nil { + CloseHyClient(dest, streamSettings) + return nil, err + } + return conn, nil +} + +func init() { + RunningClient = make(map[dialerConf]hyClient.Client) + common.Must(internet.RegisterTransportDialer(protocolName, Dial)) +} diff --git a/transport/internet/hysteria2/errors.generated.go b/transport/internet/hysteria2/errors.generated.go new file mode 100644 index 000000000..105317031 --- /dev/null +++ b/transport/internet/hysteria2/errors.generated.go @@ -0,0 +1,9 @@ +package hysteria2 + +import "github.com/v2fly/v2ray-core/v5/common/errors" + +type errPathObjHolder struct{} + +func newError(values ...interface{}) *errors.Error { + return errors.New(values...).WithPathObj(errPathObjHolder{}) +} diff --git a/transport/internet/hysteria2/hub.go b/transport/internet/hysteria2/hub.go new file mode 100644 index 000000000..9ead1c6ba --- /dev/null +++ b/transport/internet/hysteria2/hub.go @@ -0,0 +1,128 @@ +package hysteria2 + +import ( + "context" + + hyServer "github.com/v2fly/hysteria/core/v2/server" + "github.com/apernet/quic-go" + "github.com/apernet/quic-go/http3" + + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/common/net" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/tls" +) + +// Listener is an internet.Listener that listens for TCP connections. +type Listener struct { + hyServer hyServer.Server + rawConn net.PacketConn + addConn internet.ConnHandler +} + +// Addr implements internet.Listener.Addr. +func (l *Listener) Addr() net.Addr { + return l.rawConn.LocalAddr() +} + +// Close implements internet.Listener.Close. +func (l *Listener) Close() error { + return l.hyServer.Close() +} + +func (l *Listener) StreamHijacker(ft http3.FrameType, conn quic.Connection, stream quic.Stream, err error) (bool, error) { + // err always == nil + + tcpConn := &HyConn{ + stream: stream, + local: conn.LocalAddr(), + remote: conn.RemoteAddr(), + } + l.addConn(tcpConn) + return true, nil +} + +func (l *Listener) UdpHijacker(entry *hyServer.UdpSessionEntry, originalAddr string) { + addr, err := net.ResolveUDPAddr("udp", originalAddr) + if err != nil { + return + } + udpConn := &HyConn{ + IsUDPExtension: true, + IsServer: true, + ServerUDPSession: entry, + remote: addr, + local: l.rawConn.LocalAddr(), + } + l.addConn(udpConn) +} + +// 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) { + tlsConfig, err := GetServerTLSConfig(streamSettings) + if err != nil { + return nil, err + } + + if address.Family().IsDomain() { + return nil, nil + } + + 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 + } + + listener := &Listener{ + rawConn: rawConn, + addConn: handler, + } + + hyServer, err := hyServer.NewServer(&hyServer.Config{ + Conn: rawConn, + TLSConfig: *tlsConfig, + DisableUDP: !config.GetUseUdpExtension(), + Authenticator: &Authenticator{Password: config.GetPassword()}, + StreamHijacker: listener.StreamHijacker, // acceptStreams + BandwidthConfig: hyServer.BandwidthConfig{MaxTx: config.Congestion.GetUpMbps() * MBps, MaxRx: config.GetCongestion().GetDownMbps() * MBps}, + UdpSessionHijacker: listener.UdpHijacker, // acceptUDPSession + IgnoreClientBandwidth: config.GetIgnoreClientBandwidth(), + }) + if err != nil { + return nil, err + } + + listener.hyServer = hyServer + go hyServer.Serve() + return listener, nil +} + +func GetServerTLSConfig(streamSettings *internet.MemoryStreamConfig) (*hyServer.TLSConfig, error) { + config := tls.ConfigFromStreamSettings(streamSettings) + if config == nil { + return nil, newError(Hy2MustNeedTLS) + } + tlsConfig := config.GetTLSConfig() + + return &hyServer.TLSConfig{Certificates: tlsConfig.Certificates, GetCertificate: tlsConfig.GetCertificate}, nil +} + +type Authenticator struct { + Password string +} + +func (a *Authenticator) Authenticate(addr net.Addr, auth string, tx uint64) (ok bool, id string) { + if auth == a.Password || a.Password == "" { + return true, "user" + } + return false, "" +} + +func init() { + common.Must(internet.RegisterTransportListener(protocolName, Listen)) +} diff --git a/transport/internet/hysteria2/hy2_transport_test.go b/transport/internet/hysteria2/hy2_transport_test.go new file mode 100644 index 000000000..e4a5c1fde --- /dev/null +++ b/transport/internet/hysteria2/hy2_transport_test.go @@ -0,0 +1,155 @@ +package hysteria2_test + +import ( + "context" + "crypto/rand" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + + "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/protocol/tls/cert" + "github.com/v2fly/v2ray-core/v5/common/session" + "github.com/v2fly/v2ray-core/v5/testing/servers/udp" + "github.com/v2fly/v2ray-core/v5/transport/internet" + "github.com/v2fly/v2ray-core/v5/transport/internet/hysteria2" + "github.com/v2fly/v2ray-core/v5/transport/internet/tls" +) + +func TestTCP(t *testing.T) { + port := udp.PickPort() + + listener, err := hysteria2.Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{ + ProtocolName: "hysteria2", + ProtocolSettings: &hysteria2.Config{Password: "123"}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{ + tls.ParseCertificate( + cert.MustGenerate(nil, + cert.DNSNames("www.v2fly.org"), + ), + ), + }, + }, + }, func(conn internet.Connection) { + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + b.Clear() + if _, err := b.ReadFrom(conn); err != nil { + fmt.Println(err) + return + } + common.Must2(conn.Write(b.Bytes())) + } + }() + }) + common.Must(err) + + defer listener.Close() + + time.Sleep(time.Second) + + dctx := context.Background() + conn, err := hysteria2.Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "hysteria2", + ProtocolSettings: &hysteria2.Config{Password: "123"}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + ServerName: "www.v2fly.org", + AllowInsecure: true, + }, + }) + common.Must(err) + defer conn.Close() + + const N = 1000 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + b2 := buf.New() + + common.Must2(conn.Write(b1)) + + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } +} + +func TestUDP(t *testing.T) { + port := udp.PickPort() + + listener, err := hysteria2.Listen(context.Background(), net.LocalHostIP, port, &internet.MemoryStreamConfig{ + ProtocolName: "hysteria2", + ProtocolSettings: &hysteria2.Config{Password: "123", UseUdpExtension: true}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + Certificate: []*tls.Certificate{ + tls.ParseCertificate( + cert.MustGenerate(nil, + cert.DNSNames("www.v2fly.org"), + ), + ), + }, + }, + }, func(conn internet.Connection) { + fmt.Println("incoming") + go func() { + defer conn.Close() + + b := buf.New() + defer b.Release() + + for { + b.Clear() + if _, err := b.ReadFrom(conn); err != nil { + fmt.Println(err) + return + } + common.Must2(conn.Write(b.Bytes())) + } + }() + }) + common.Must(err) + + defer listener.Close() + + time.Sleep(time.Second) + + address, err := net.ParseDestination("udp:127.0.0.1:1180") + dctx := session.ContextWithOutbound(context.Background(), &session.Outbound{Target: address}) + + conn, err := hysteria2.Dial(dctx, net.TCPDestination(net.LocalHostIP, port), &internet.MemoryStreamConfig{ + ProtocolName: "hysteria2", + ProtocolSettings: &hysteria2.Config{Password: "123", UseUdpExtension: true}, + SecurityType: "tls", + SecuritySettings: &tls.Config{ + ServerName: "www.v2fly.org", + AllowInsecure: true, + }, + }) + common.Must(err) + defer conn.Close() + + const N = 1000 + b1 := make([]byte, N) + common.Must2(rand.Read(b1)) + common.Must2(conn.Write(b1)) + + b2 := buf.New() + b2.Clear() + common.Must2(b2.ReadFullFrom(conn, N)) + if r := cmp.Diff(b2.Bytes(), b1); r != "" { + t.Error(r) + } +} diff --git a/transport/internet/hysteria2/hysteria2.go b/transport/internet/hysteria2/hysteria2.go new file mode 100644 index 000000000..031c37763 --- /dev/null +++ b/transport/internet/hysteria2/hysteria2.go @@ -0,0 +1,18 @@ +package hysteria2 + +import ( + "github.com/v2fly/v2ray-core/v5/common" + "github.com/v2fly/v2ray-core/v5/transport/internet" +) + +//go:generate go run github.com/v2fly/v2ray-core/v5/common/errors/errorgen + +const ( + protocolName = "hysteria2" +) + +func init() { + common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} { + return new(Config) + })) +}