@@ -109,12 +109,58 @@ fun parseHysteria2(url: String): HysteriaBean {
109109 link.queryParameter(" obfs-max-packet-size" )?.toIntOrNull()?.also {
110110 geckoMaxPacketSize = it
111111 }
112+ queryInt(link, " initStreamReceiveWindow" , " init_stream_receive_window" , " init-stream-receive-window" )?.also {
113+ hy2InitialStreamReceiveWindow = it
114+ }
115+ queryInt(link, " maxStreamReceiveWindow" , " max_stream_receive_window" , " max-stream-receive-window" )?.also {
116+ hy2MaxStreamReceiveWindow = it
117+ }
118+ queryInt(link, " initConnReceiveWindow" , " init_conn_receive_window" , " init-conn-receive-window" )?.also {
119+ hy2InitialConnectionReceiveWindow = it
120+ }
121+ queryInt(link, " maxConnReceiveWindow" , " max_conn_receive_window" , " max-conn-receive-window" )?.also {
122+ hy2MaxConnectionReceiveWindow = it
123+ }
124+ querySeconds(link, " maxIdleTimeout" , " max_idle_timeout" , " max-idle-timeout" )?.also {
125+ hy2MaxIdleTimeout = it
126+ }
127+ querySeconds(link, " keepAlivePeriod" , " keep_alive_period" , " keep-alive-period" )?.also {
128+ hy2KeepAlivePeriod = it
129+ }
130+ queryBool(link, " disablePathMTUDiscovery" , " disable_path_mtu_discovery" , " disable-path-mtu-discovery" )?.also {
131+ disableMtuDiscovery = it
132+ }
133+ querySeconds(link, " hopInterval" , " hop_interval" , " hop-interval" )?.also {
134+ hopInterval = it
135+ }
136+ querySeconds(link, " minHopInterval" , " min_hop_interval" , " min-hop-interval" )?.also {
137+ hy2MinHopInterval = it
138+ }
139+ querySeconds(link, " maxHopInterval" , " max_hop_interval" , " max-hop-interval" )?.also {
140+ hy2MaxHopInterval = it
141+ }
112142// link.queryParameter("pinSHA256")?.also {
113143// // TODO your box do not support it
114144// }
115145 }
116146}
117147
148+ private fun queryInt (link : okhttp3.HttpUrl , vararg names : String ): Int? {
149+ return names.firstNotNullOfOrNull { link.queryParameter(it)?.toIntOrNull() }
150+ }
151+
152+ private fun queryBool (link : okhttp3.HttpUrl , vararg names : String ): Boolean? {
153+ return names.firstNotNullOfOrNull { name ->
154+ link.queryParameter(name)?.let { it == " 1" || it.equals(" true" , ignoreCase = true ) }
155+ }
156+ }
157+
158+ private fun querySeconds (link : okhttp3.HttpUrl , vararg names : String ): Int? {
159+ return names.firstNotNullOfOrNull { name ->
160+ link.queryParameter(name)?.trim()?.removeSuffix(" s" )?.toIntOrNull()
161+ }
162+ }
163+
118164fun HysteriaBean.toUri (): String {
119165 var un = " "
120166 var pw = " "
@@ -185,6 +231,35 @@ fun HysteriaBean.toUri(): String {
185231 }
186232 }
187233 }
234+ if (hy2InitialStreamReceiveWindow > 0 ) {
235+ builder.addQueryParameter(" initStreamReceiveWindow" , hy2InitialStreamReceiveWindow.toString())
236+ }
237+ if (hy2MaxStreamReceiveWindow > 0 ) {
238+ builder.addQueryParameter(" maxStreamReceiveWindow" , hy2MaxStreamReceiveWindow.toString())
239+ }
240+ if (hy2InitialConnectionReceiveWindow > 0 ) {
241+ builder.addQueryParameter(" initConnReceiveWindow" , hy2InitialConnectionReceiveWindow.toString())
242+ }
243+ if (hy2MaxConnectionReceiveWindow > 0 ) {
244+ builder.addQueryParameter(" maxConnReceiveWindow" , hy2MaxConnectionReceiveWindow.toString())
245+ }
246+ if (hy2MaxIdleTimeout > 0 ) {
247+ builder.addQueryParameter(" maxIdleTimeout" , " ${hy2MaxIdleTimeout} s" )
248+ }
249+ if (hy2KeepAlivePeriod > 0 ) {
250+ builder.addQueryParameter(" keepAlivePeriod" , " ${hy2KeepAlivePeriod} s" )
251+ }
252+ if (disableMtuDiscovery) {
253+ builder.addQueryParameter(" disablePathMTUDiscovery" , " 1" )
254+ }
255+ if (isMultiPort(displayAddress())) {
256+ if (hy2MinHopInterval > 0 || hy2MaxHopInterval > 0 ) {
257+ if (hy2MinHopInterval > 0 ) builder.addQueryParameter(" minHopInterval" , " ${hy2MinHopInterval} s" )
258+ if (hy2MaxHopInterval > 0 ) builder.addQueryParameter(" maxHopInterval" , " ${hy2MaxHopInterval} s" )
259+ } else if (hopInterval != 10 ) {
260+ builder.addQueryParameter(" hopInterval" , " ${hopInterval} s" )
261+ }
262+ }
188263 }
189264 return builder.toLink(if (protocolVersion == 2 ) " hy2" else " hysteria" )
190265}
@@ -289,16 +364,35 @@ fun isMultiPort(hyAddr: String): Boolean {
289364}
290365
291366fun getFirstPort (portStr : String ): Int {
292- return portStr.substringBefore(" : " ).substringBefore(" , " ).toIntOrNull() ? : 443
367+ return portStr.substringBefore(" , " ).substringBefore(" - " ).substringBefore( " : " ).toIntOrNull() ? : 443
293368}
294369
295370fun HysteriaBean.canUseSingBox (): Boolean {
296371 if (protocol != HysteriaBean .PROTOCOL_UDP ) return false
372+ if (protocolVersion == 2 && hasAdvancedHysteria2Options()) {
373+ // The bundled sing-box Hysteria2 option model supports Gecko obfs and fixed
374+ // port hopping, but not Hysteria's advanced QUIC knobs or random hop
375+ // intervals. Use the Hysteria2 sidecar when those fields are configured.
376+ return false
377+ }
297378 // Gecko obfs is now supported natively by this fork's sing-box core (via the
298- // hawkff/sing-quic gecko backport), so it no longer needs the sidecar.
379+ // hawkff/sing-quic gecko backport), so default HY2 profiles do not need the sidecar.
299380 return true
300381}
301382
383+ fun HysteriaBean.hasAdvancedHysteria2Options (): Boolean {
384+ if (protocolVersion != 2 ) return false
385+ return hy2InitialStreamReceiveWindow > 0 ||
386+ hy2MaxStreamReceiveWindow > 0 ||
387+ hy2InitialConnectionReceiveWindow > 0 ||
388+ hy2MaxConnectionReceiveWindow > 0 ||
389+ hy2MaxIdleTimeout > 0 ||
390+ hy2KeepAlivePeriod > 0 ||
391+ disableMtuDiscovery ||
392+ hy2MinHopInterval > 0 ||
393+ hy2MaxHopInterval > 0
394+ }
395+
302396fun buildSingBoxOutboundHysteriaBean (bean : HysteriaBean ): SingBoxOptions .SingBoxOption {
303397 return when (bean.protocolVersion) {
304398 1 -> SingBoxOptions .Outbound_HysteriaOptions ().apply {
@@ -413,9 +507,9 @@ fun hopPortsToSingboxList(s: String): List<String> {
413507/* *
414508 * Builds a config for the bundled official apernet/hysteria client binary (sidecar).
415509 *
416- * NOTE: Hysteria2 (including Gecko obfs) now runs natively in the sing-box core, so this
417- * sidecar path is no longer used for Gecko. This builder is retained for the bundled
418- * hysteria2 binary infrastructure and potential fallback use .
510+ * NOTE: Default Hysteria2 (including Gecko obfs and fixed port hopping) runs natively in
511+ * the sing-box core. This sidecar path is used when HY2 advanced QUIC options or random
512+ * hop intervals are configured, because those fields are not exposed by the pinned core .
419513 *
420514 * Emitted as JSON (hysteria uses viper, which detects format by the .json extension).
421515 * The client's upstream QUIC sockets are protected from the VPN via
@@ -471,7 +565,30 @@ fun HysteriaBean.buildHysteria2SidecarConfig(
471565 if (downloadMbps > 0 ) put(" down" , " $downloadMbps mbps" )
472566 })
473567 }
568+ if (isMultiPort(displayAddress()) || hy2MinHopInterval > 0 || hy2MaxHopInterval > 0 ) {
569+ put(" transport" , JSONObject ().apply {
570+ put(" type" , " udp" )
571+ put(" udp" , JSONObject ().apply {
572+ if (hy2MinHopInterval > 0 || hy2MaxHopInterval > 0 ) {
573+ val rawMin = hy2MinHopInterval.takeIf { it > 0 } ? : hy2MaxHopInterval
574+ val min = rawMin.coerceAtLeast(5 )
575+ val max = (hy2MaxHopInterval.takeIf { it > 0 } ? : min).coerceAtLeast(min)
576+ put(" minHopInterval" , " ${min} s" )
577+ put(" maxHopInterval" , " ${max} s" )
578+ } else if (hopInterval > 0 ) {
579+ put(" hopInterval" , " ${hopInterval.coerceAtLeast(5 )} s" )
580+ }
581+ })
582+ })
583+ }
474584 put(" quic" , JSONObject ().apply {
585+ if (hy2InitialStreamReceiveWindow > 0 ) put(" initStreamReceiveWindow" , hy2InitialStreamReceiveWindow)
586+ if (hy2MaxStreamReceiveWindow > 0 ) put(" maxStreamReceiveWindow" , hy2MaxStreamReceiveWindow)
587+ if (hy2InitialConnectionReceiveWindow > 0 ) put(" initConnReceiveWindow" , hy2InitialConnectionReceiveWindow)
588+ if (hy2MaxConnectionReceiveWindow > 0 ) put(" maxConnReceiveWindow" , hy2MaxConnectionReceiveWindow)
589+ if (hy2MaxIdleTimeout > 0 ) put(" maxIdleTimeout" , " ${hy2MaxIdleTimeout} s" )
590+ if (hy2KeepAlivePeriod > 0 ) put(" keepAlivePeriod" , " ${hy2KeepAlivePeriod} s" )
591+ if (disableMtuDiscovery) put(" disablePathMTUDiscovery" , true )
475592 put(" sockopts" , JSONObject ().apply {
476593 put(" fdControlUnixSocket" , protectPath)
477594 })
0 commit comments