Skip to content

New feature: Happy Eyeballs (RFC 8305) #4667

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

patterniha
Copy link
Contributor

@patterniha patterniha commented Apr 29, 2025

when using "AsIs" mode, golang controls how to connect to a "domain" address.

currently, golang use RFC-6555 to connect to the domains, although it is obsolete, but it is better than nothing. it solve the problem when one IP-type is unreachable. but regardless of the IP type, some IPs may not be available, thus RFC 8305 was created #4473 (comment)

RFC 6555 is obsoleted by RFC 8305, and python use RFC-8305 from v3.8.

///

anyway, It doesn't matter what RFC is implemented for the "AsIs" mode, because we should use built-in-dns to bypass GFW DNS spoofing,
and for "useIP/forceIP" mode, we don't even have RFC-6555, and only one random IP is selected.

and also Iran-GFW blocked some range-IPs of meta(facebook, instagram,...) and some not, so with happy-eyeballs iranian-users can access instagram without using any server.(in 99% cases)

as a result, I implemented Happy Eyeballs RFC-8305 for "forceIP/useIP" mode.

///

because happy eyeballs only applies when sockopt-domainStrategy is "forceIP/useIP" and this options are in "sockopt" settings, so i put "happyEyeballs" settings in "sockopt" settings.

///

we can use happy eyeballs for all type of proxy: freedom, vless, ...

///

"streamSettings": {
  "sockopt": {
    "domainStrategy": "forceIP",
      "happyEyeballs": {
              "prioritizeIPv6": false, 
              "maxConcurrentTry": 4,
              "tryDelayMs": 250,
              "interleave": 1
       }
     }
   } 

///

prioritizeIPv6 ( bool): indicate "First Address Family" in RFC-8305, default is false(= prioritizeIPv4)

interleave (uint32): indicate "First Address Family count" in RFC-8305, default is 1

maxConcurrentTry (uint32): maximum concurrent attempt (this is only maximum and in most cases our concurrent attempts is less, unless all connection fail to connect) also we can always have a maximum of concurrent-attempt as many IPs as we have, and this option is useful when the number of IPs is too high, and we want to control the number of concurrent-attempts, default is 4. if it is 0, happy-eyeballs is disabled.

tryDelayMs: delay time between each attempt, RFC-8305 recommend 250ms, so the default is 250. if it is 0, happy-eyeballs is disabled.

///

for example suppose our IP-list is [ip4-1, ip4-2, ip4-3, ip4-4, ip6-1, ip6-2, ip6-3, ip6-4]

when interleave is 1 and prioritizeIPv6 is false, the sorted-ip-list is:
[ip4-1, ip6-1, ip4-2, ip6-2, ip4-3, ip6-3, ip4-4, ip6-4]

and when for example interleave is 2 and prioritizeIPv6 is true:
[ip6-1, ip6-2, ip4-1, ip4-2, ip6-3, ip6-4, ip4-3, ip4-4]

then delay 250ms for each attempt until first connection is established.

the first-stablished-connection is winner connection and selected for sending/receiving data.

@Fangliding
Copy link
Member

Fangliding commented Apr 29, 2025

最好只留一个delay,大于0代表启用,别的取默认就行了。结构体套娃最好少一点。maxConcurrentTry 4够了 算上 v6 8也够了,或者智能一点,单栈取4双栈取8,它的主要作用是遇到奇葩域名别一次性dial出去几十个连接。interleave选1就好。prioritizeIPv6字段没必要。prefer从前面的domainstrategy获取,比如 ForceIPv4v6 就是 prefer v4
golang在单栈机器上dial不支持的IP类型失败会快速返回一个unreachable,可以借此快速开启下一轮重试并在以后跳过不支持的ipversion(不干保持简洁也行,用户自己配的prefer弄错了是他们自己的问题)
<-time.After(waitTime) 这样会导致每次进result之后等待时间重置。用time.Ticker就可以了,也不用很奇怪的设置为等待100小时了,直接Stop()就行

@patterniha
Copy link
Contributor Author

patterniha commented Apr 29, 2025

@Fangliding

first, thanks for your feedback and reviewing my code.

  1. we have to set domainStrategy to forceIP to have both IPv4 and IPv6, so we can't delete prioritizeIPv6 option (if we set ForceIPv4v6 we only have IPv4, so we can't try IPv6, unless the domain does not have IPv4!)

    ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{
    IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.preferIP4(),
    IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.preferIP6(),
    })

  2. interleave option is "First Address Family Count" in RFC-8305 and even in python we can customize that:
    https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection
    also, suppose we want to try all IPv6 before IPv4, so we can set interleave to a large value for example 100, and then we make sure all the IPv6 are tested before the IPv4, so we need interleave option.

  3. although maxConcurrentTry = 4 is enough for most cases, but we may need to increase that in some special cases (for example finding a healthy IP from a bunch of IPs), so it is better to keep that.(also, i want to increase that in serverless-for-Iran)

  4. we can't remove enabled option, because the default value for tryDelayMs is 250, so if a user omits to set this option, we should set it to 250, but golang json.Unmarshal does not distinguish between "not setting tryDelayMs option" and "setting tryDelayMs = 0".

  5. You're right, "wait for 100 hours" was weird, but we need Timer instead of Ticker in this algorithm, so i use Timer instead of time.After, thx.

  6. I enable happy-eyeballs by default in new commit, because in AsIs mode happy-eyeballs is enabled by default (although it is still RFC-6555), also, this is a useful option that all users should take advantage of.

@Fangliding
Copy link
Member

Fangliding commented Apr 30, 2025

@Fangliding

first, thanks for your feedback and reviewing my code.

  1. we have to set domainStrategy to forceIP to have both IPv4 and IPv6, so we can't delete prioritizeIPv6 option (if we set ForceIPv4v6 we only have IPv4, so we can't try IPv6, unless the domain does not have IPv4!)
    ips, _, err := dnsClient.LookupIP(domain, dns.IPOption{
    IPv4Enable: (localAddr == nil || localAddr.Family().IsIPv4()) && strategy.preferIP4(),
    IPv6Enable: (localAddr == nil || localAddr.Family().IsIPv6()) && strategy.preferIP6(),
    })

只查v4/6是之前结构下的无奈之举 因为只能随机roll一个IP出来 在这种情况下可以手动两种都查

  1. interleave option is "First Address Family Count" in RFC-8305 and even in python we can customize that:
    https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection
    also, suppose we want to try all IPv6 before IPv4, so we can set interleave to a large value for example 100, and then we make sure all the IPv6 are tested before the IPv4, so we need interleave option.

python选项里有不代表要搬到golang的xray的config里来 我实在看不到这有啥配置的必要

  1. although maxConcurrentTry = 4 is enough for most cases, but we may need to increase that in some special cases (for example finding a healthy IP from a bunch of IPs), so it is better to keep that.(also, i want to increase that in serverless-for-Iran)

很多时候一个域名下面应该不会存在那么多的IP 大于4个的A记录和AAAA都挺少见

  1. we can't remove enabled option, because the default value for tryDelayMs is 250, so if a user omits to set this option, we should set it to 250, but golang json.Unmarshal does not distinguish between "not setting tryDelayMs option" and "setting tryDelayMs = 0".

设置为0禁用或者启用一个功能很常见 如果真的想快速发出一堆请求 设置为1并没有什么不同

说这么多主要目的还是为了压缩到只剩一个选项 当然只是我觉得这样更好

@patterniha
Copy link
Contributor Author

I explained the necessity of each of the options.
Xray-core is CORE, It should cover all types of uses.

@RPRX
Copy link
Member

RPRX commented Apr 30, 2025

看起来这个 PR 还在讨论中,所以四月累积更新版本暂不包含这个 PR

@patterniha
Copy link
Contributor Author

patterniha commented Apr 30, 2025

It seems that this PR is still under discussion, so the April cumulative update version does not include this PR for the time being.

@RPRX

Even @Fangliding accepted that.

But he says to remove the extra-options and I explained to him why these options are needed, there is no other difference.

Changing happy-eyeballs default parameters is useful for serverless-for-Iran (my main goal) and other goals.

So please merge it as soon as possible, I have written these for almost a month.
And i need these two PRs for serverless-for-Iran-anti-sanction-version.

@patterniha
Copy link
Contributor Author

patterniha commented Apr 30, 2025

@RPRX

happy-eyeballs is a feature that most users don't need to know about, It is enabled by default for everyone and increases the quality of their Internet usage.

They don't need to change anything in the configuration.

It is true that the default values ​​are sufficient for most users, but in many cases they need to be changed, so the existence of these options is essential.

@RPRX
Copy link
Member

RPRX commented May 1, 2025

如果 Chrome 已经有了这样的行为则 Xray 可以默认改成一样的行为

@patterniha
Copy link
Contributor Author

patterniha commented May 1, 2025

@RPRX

If you are worried about Detection by firewall and different behavior
Even golang-built-in-happy-eyeballs behaves differently with Chrome.
So you should even remove AsIs mode.

Each connection Is independent and each connection behavior is similar to Chrome.

So there is no need to worry about this.

@RPRX
Copy link
Member

RPRX commented May 1, 2025

只是 Xray 不应出现过于独特的行为,要么 Golang 要么 Chrome 要么非默认

@patterniha
Copy link
Contributor Author

patterniha commented May 1, 2025

@RPRX

I implemented rfc-8305 exactly, and this is exactly python happy-eyeballs.

So the behavior is exactly Python's behavior.

@patterniha
Copy link
Contributor Author

patterniha commented May 1, 2025

@RPRX

https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.create_connection

I already looked at the Python codes, and my happy-eyeballs is exactly python's happy-eyeballs.

@patterniha
Copy link
Contributor Author

patterniha commented May 9, 2025

@Fangliding

i remove enabled option and if tryDelayMs or maxConcurrentTry is 0, happy-eyeballs is disabled.

any other questions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants