mirror of
https://github.com/MatsuriDayo/NekoBoxForAndroid.git
synced 2025-12-18 22:20:06 +08:00
optimize subscription import
This commit is contained in:
parent
893ba090f6
commit
0127c60906
@ -29,6 +29,7 @@ import kotlinx.coroutines.DEBUG_PROPERTY_NAME
|
||||
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
|
||||
import libcore.Libcore
|
||||
import moe.matsuri.nb4a.NativeInterface
|
||||
import moe.matsuri.nb4a.net.LocalResolverImpl
|
||||
import moe.matsuri.nb4a.utils.JavaUtil
|
||||
import moe.matsuri.nb4a.utils.cleanWebview
|
||||
import java.io.File
|
||||
@ -78,7 +79,7 @@ class SagerNet : Application(),
|
||||
externalAssets.absolutePath + "/",
|
||||
DataStore.logBufSize,
|
||||
DataStore.logLevel > 0,
|
||||
nativeInterface, nativeInterface
|
||||
nativeInterface, nativeInterface, LocalResolverImpl
|
||||
)
|
||||
|
||||
if (isMainProcess) {
|
||||
|
||||
@ -58,6 +58,7 @@ object RawUpdater : GroupUpdater() {
|
||||
|
||||
val response = Libcore.newHttpClient().apply {
|
||||
trySocks5(DataStore.mixedPort)
|
||||
tryH3Direct()
|
||||
when (DataStore.appTLSVersion) {
|
||||
"1.3" -> restrictedTLS()
|
||||
}
|
||||
@ -73,6 +74,17 @@ object RawUpdater : GroupUpdater() {
|
||||
|
||||
subscription.subscriptionUserinfo =
|
||||
Util.getStringBox(response.getHeader("Subscription-Userinfo"))
|
||||
|
||||
// 修改默认名字
|
||||
if (proxyGroup.name?.startsWith("Subscription #") == true) {
|
||||
var remoteName = Util.getStringBox(response.getHeader("content-disposition"))
|
||||
if (remoteName.isNotBlank()) {
|
||||
remoteName = Util.decodeFilename(remoteName)
|
||||
if (remoteName.isNotBlank()) {
|
||||
proxyGroup.name = remoteName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val proxiesMap = LinkedHashMap<String, AbstractBean>()
|
||||
|
||||
@ -216,7 +216,6 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
|
||||
try {
|
||||
val client = Libcore.newHttpClient().apply {
|
||||
modernTLS()
|
||||
keepAlive()
|
||||
trySocks5(DataStore.mixedPort)
|
||||
}
|
||||
val response = client.newRequest().apply {
|
||||
|
||||
@ -5,6 +5,8 @@ import android.content.Context
|
||||
import android.util.Base64
|
||||
import libcore.StringBox
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.net.URLDecoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.Deflater
|
||||
@ -188,4 +190,11 @@ object Util {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun decodeFilename(headerValue: String): String {
|
||||
val regex = Regex("filename\\*=[^']*''(.+)")
|
||||
val match = regex.find(headerValue)
|
||||
val encoded = match?.groupValues?.get(1) ?: ""
|
||||
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name())
|
||||
}
|
||||
}
|
||||
|
||||
@ -25,6 +25,8 @@ type LocalDNSTransport interface {
|
||||
Exchange(ctx *ExchangeContext, message []byte) error
|
||||
}
|
||||
|
||||
var gLocalDNSTransport *platformLocalDNSTransport = nil
|
||||
|
||||
type platformLocalDNSTransport struct {
|
||||
iif LocalDNSTransport
|
||||
tag string
|
||||
|
||||
83
libcore/ech/ech.go
Normal file
83
libcore/ech/ech.go
Normal file
@ -0,0 +1,83 @@
|
||||
package ech
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing/common/exceptions"
|
||||
)
|
||||
|
||||
type ECHClientConfig struct {
|
||||
*tls.Config
|
||||
domain string
|
||||
localDnsTransport adapter.DNSTransport
|
||||
}
|
||||
|
||||
func NewECHClientConfig(domain string, tlsConfig *tls.Config, localDnsTransport adapter.DNSTransport) *ECHClientConfig {
|
||||
config := tlsConfig.Clone()
|
||||
config.ServerName = domain
|
||||
return &ECHClientConfig{
|
||||
Config: config,
|
||||
domain: domain,
|
||||
localDnsTransport: localDnsTransport,
|
||||
}
|
||||
}
|
||||
|
||||
// ClientHandshake 封装 TLS 握手
|
||||
func (s *ECHClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (*tls.Conn, error) {
|
||||
tlsConn, err := s.fetchAndHandshake(ctx, conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = tlsConn.HandshakeContext(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
|
||||
// fetchAndHandshake 查询 ECHConfigList 并完成 TLS 连接
|
||||
func (s *ECHClientConfig) fetchAndHandshake(ctx context.Context, conn net.Conn) (*tls.Conn, error) {
|
||||
message := &mDNS.Msg{
|
||||
MsgHdr: mDNS.MsgHdr{
|
||||
RecursionDesired: true,
|
||||
},
|
||||
Question: []mDNS.Question{
|
||||
{
|
||||
Name: mDNS.Fqdn(s.domain),
|
||||
Qtype: mDNS.TypeHTTPS,
|
||||
Qclass: mDNS.ClassINET,
|
||||
},
|
||||
},
|
||||
}
|
||||
if s.localDnsTransport == nil {
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
response, err := s.localDnsTransport.Exchange(ctx, message)
|
||||
if err != nil {
|
||||
return nil, exceptions.Cause(err, "fetch ECH config list")
|
||||
}
|
||||
if response.Rcode != mDNS.RcodeSuccess {
|
||||
return nil, exceptions.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
|
||||
}
|
||||
for _, rr := range response.Answer {
|
||||
switch resource := rr.(type) {
|
||||
case *mDNS.HTTPS:
|
||||
for _, value := range resource.Value {
|
||||
if value.Key().String() == "ech" {
|
||||
echConfigList, err := base64.StdEncoding.DecodeString(value.String())
|
||||
if err == nil {
|
||||
s.Config.EncryptedClientHelloConfigList = echConfigList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tls.Client(conn, s.Config), nil
|
||||
}
|
||||
@ -8,6 +8,7 @@ require (
|
||||
github.com/matsuridayo/libneko v1.0.0 // replaced
|
||||
github.com/miekg/dns v1.1.67
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/sagernet/quic-go v0.52.0-beta.1
|
||||
github.com/sagernet/sing v0.7.6-0.20250825114712-2aeec120ce28
|
||||
github.com/sagernet/sing-box v1.0.0 // replaced
|
||||
github.com/sagernet/sing-tun v0.7.0-beta.1
|
||||
@ -51,7 +52,6 @@ require (
|
||||
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb // indirect
|
||||
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
|
||||
github.com/sagernet/quic-go v0.52.0-beta.1 // indirect
|
||||
github.com/sagernet/sing-mux v0.3.3 // indirect
|
||||
github.com/sagernet/sing-quic v0.5.0 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
|
||||
|
||||
176
libcore/http.go
176
libcore/http.go
@ -10,24 +10,34 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"libcore/device"
|
||||
"libcore/ech"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
"github.com/sagernet/sing/common/metadata"
|
||||
"github.com/sagernet/sing/protocol/socks"
|
||||
"github.com/sagernet/sing/protocol/socks/socks5"
|
||||
)
|
||||
|
||||
var errFailConnectSocks5 = errors.New("fail connect socks5")
|
||||
|
||||
type HTTPClient interface {
|
||||
RestrictedTLS()
|
||||
ModernTLS()
|
||||
PinnedTLS12()
|
||||
PinnedSHA256(sumHex string)
|
||||
TrySocks5(port int32)
|
||||
TryH3Direct()
|
||||
KeepAlive()
|
||||
NewRequest() HTTPRequest
|
||||
Close()
|
||||
@ -58,16 +68,18 @@ var (
|
||||
)
|
||||
|
||||
type httpClient struct {
|
||||
tls tls.Config
|
||||
client http.Client
|
||||
transport http.Transport
|
||||
tls tls.Config
|
||||
h1h2Transport http.Transport
|
||||
h1h2Client http.Client
|
||||
trySocks5 bool
|
||||
tryH3Direct bool
|
||||
}
|
||||
|
||||
func NewHttpClient() HTTPClient {
|
||||
client := new(httpClient)
|
||||
client.client.Transport = &client.transport
|
||||
client.transport.TLSClientConfig = &client.tls
|
||||
client.transport.DisableKeepAlives = true
|
||||
client.h1h2Client.Transport = &client.h1h2Transport
|
||||
client.h1h2Transport.TLSClientConfig = &client.tls
|
||||
client.h1h2Transport.DisableKeepAlives = true
|
||||
return client
|
||||
}
|
||||
|
||||
@ -104,25 +116,36 @@ func (c *httpClient) PinnedSHA256(sumHex string) {
|
||||
|
||||
func (c *httpClient) TrySocks5(port int32) {
|
||||
dialer := new(net.Dialer)
|
||||
c.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
c.h1h2Transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
for {
|
||||
socksConn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(port)))
|
||||
if err != nil {
|
||||
if c.tryH3Direct {
|
||||
return nil, errFailConnectSocks5
|
||||
}
|
||||
break
|
||||
}
|
||||
_, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, metadata.ParseSocksaddr(addr), "", "")
|
||||
if err != nil {
|
||||
if c.tryH3Direct {
|
||||
return nil, errFailConnectSocks5
|
||||
}
|
||||
break
|
||||
}
|
||||
return socksConn, err
|
||||
}
|
||||
return dialer.DialContext(ctx, network, addr)
|
||||
}
|
||||
c.trySocks5 = true
|
||||
}
|
||||
|
||||
func (c *httpClient) TryH3Direct() {
|
||||
c.tryH3Direct = true
|
||||
}
|
||||
|
||||
func (c *httpClient) KeepAlive() {
|
||||
c.transport.ForceAttemptHTTP2 = true
|
||||
c.transport.DisableKeepAlives = false
|
||||
c.h1h2Transport.ForceAttemptHTTP2 = true
|
||||
c.h1h2Transport.DisableKeepAlives = false
|
||||
}
|
||||
|
||||
func (c *httpClient) NewRequest() HTTPRequest {
|
||||
@ -135,7 +158,7 @@ func (c *httpClient) NewRequest() HTTPRequest {
|
||||
}
|
||||
|
||||
func (c *httpClient) Close() {
|
||||
c.transport.CloseIdleConnections()
|
||||
c.h1h2Transport.CloseIdleConnections()
|
||||
}
|
||||
|
||||
type httpRequest struct {
|
||||
@ -184,8 +207,16 @@ func (r *httpRequest) SetContentString(content string) {
|
||||
}
|
||||
|
||||
func (r *httpRequest) Execute() (HTTPResponse, error) {
|
||||
response, err := r.client.Do(&r.request)
|
||||
// full direct
|
||||
if r.tryH3Direct && !r.trySocks5 {
|
||||
return r.doH3Direct()
|
||||
}
|
||||
response, err := r.h1h2Client.Do(&r.request)
|
||||
if err != nil {
|
||||
// trySocks5 && tryH3Direct
|
||||
if r.tryH3Direct && errors.Is(err, errFailConnectSocks5) {
|
||||
return r.doH3Direct()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
httpResp := &httpResponse{Response: response}
|
||||
@ -195,6 +226,129 @@ func (r *httpRequest) Execute() (HTTPResponse, error) {
|
||||
return httpResp, nil
|
||||
}
|
||||
|
||||
type requestFunc func() (response *http.Response, err error)
|
||||
|
||||
func (r *httpRequest) doH3Direct() (HTTPResponse, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
successCh := make(chan *http.Response, 1)
|
||||
var finalErr error
|
||||
var failedCount atomic.Uint32
|
||||
var successCount atomic.Uint32
|
||||
var mu sync.Mutex
|
||||
|
||||
funcs := []requestFunc{
|
||||
// 普通,不再重试 socks5
|
||||
func() (response *http.Response, err error) {
|
||||
request := r.request.Clone(context.Background())
|
||||
h1h2Client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
}
|
||||
return h1h2Client.Do(request)
|
||||
},
|
||||
// ECH HTTPS
|
||||
func() (response *http.Response, err error) {
|
||||
request := r.request.Clone(context.Background())
|
||||
echClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
c, err := d.DialContext(ctx, network, addr)
|
||||
if err != nil {
|
||||
return c, err
|
||||
}
|
||||
domain := addr
|
||||
if host, _, _ := net.SplitHostPort(addr); host != "" {
|
||||
domain = host
|
||||
}
|
||||
echTls := ech.NewECHClientConfig(domain, &r.tls, gLocalDNSTransport)
|
||||
return echTls.ClientHandshake(ctx, c)
|
||||
},
|
||||
DisableKeepAlives: true,
|
||||
},
|
||||
}
|
||||
return echClient.Do(request)
|
||||
},
|
||||
// H3
|
||||
func() (response *http.Response, err error) {
|
||||
request := r.request.Clone(context.Background())
|
||||
h3Client := &http.Client{
|
||||
Transport: &http3.Transport{
|
||||
TLSClientConfig: r.tls.Clone(),
|
||||
QUICConfig: &quic.Config{
|
||||
MaxIdleTimeout: time.Second,
|
||||
},
|
||||
},
|
||||
}
|
||||
return h3Client.Do(request)
|
||||
},
|
||||
}
|
||||
|
||||
for i, f := range funcs {
|
||||
go func(f requestFunc) {
|
||||
defer device.DeferPanicToError("http", func(err error) {
|
||||
log.Println(err)
|
||||
})
|
||||
defer func() {
|
||||
if successCount.Load() == 0 {
|
||||
if failedCount.Add(1) >= uint32(len(funcs)) {
|
||||
// 全部失败了
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var t string
|
||||
switch i {
|
||||
case 0:
|
||||
t = "h1h2"
|
||||
case 1:
|
||||
t = "ech"
|
||||
case 2:
|
||||
t = "h3"
|
||||
}
|
||||
|
||||
// 执行HTTP请求
|
||||
rsp, err := f()
|
||||
if rsp == nil || err != nil {
|
||||
mu.Lock()
|
||||
finalErr = errors.Join(finalErr, fmt.Errorf("%s: %w", t, err))
|
||||
mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// 处理 HTTP 状态码
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
hr := &httpResponse{Response: rsp}
|
||||
err = errors.Join(finalErr, fmt.Errorf("%s: %s", t, hr.errorString()))
|
||||
mu.Lock()
|
||||
finalErr = err
|
||||
mu.Unlock()
|
||||
rsp.Body.Close()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case successCh <- rsp:
|
||||
// 第一个成功的请求,不要关闭 body
|
||||
successCount.Add(1)
|
||||
default:
|
||||
rsp.Body.Close()
|
||||
}
|
||||
}(f)
|
||||
}
|
||||
|
||||
select {
|
||||
case result := <-successCh:
|
||||
return &httpResponse{Response: result}, nil
|
||||
case <-ctx.Done():
|
||||
return nil, finalErr
|
||||
}
|
||||
}
|
||||
|
||||
type httpResponse struct {
|
||||
*http.Response
|
||||
|
||||
|
||||
@ -34,7 +34,7 @@ func ForceGc() {
|
||||
|
||||
func InitCore(process, cachePath, internalAssets, externalAssets string,
|
||||
maxLogSizeKb int32, logEnable bool,
|
||||
if1 NB4AInterface, if2 BoxPlatformInterface,
|
||||
if1 NB4AInterface, if2 BoxPlatformInterface, if3 LocalDNSTransport,
|
||||
) {
|
||||
defer device.DeferPanicToError("InitCore", func(err error) { log.Println(err) })
|
||||
isBgProcess = strings.HasSuffix(process, ":bg")
|
||||
@ -43,6 +43,7 @@ func InitCore(process, cachePath, internalAssets, externalAssets string,
|
||||
intfNB4A = if1
|
||||
intfBox = if2
|
||||
useProcfs = intfBox.UseProcFS()
|
||||
gLocalDNSTransport = &platformLocalDNSTransport{iif: if3}
|
||||
|
||||
// Working dir
|
||||
tmp := filepath.Join(cachePath, "../no_backup")
|
||||
|
||||
Loading…
Reference in New Issue
Block a user