optimize subscription import

This commit is contained in:
armv9 2025-09-03 15:00:48 +09:00
parent 893ba090f6
commit 0127c60906
9 changed files with 276 additions and 15 deletions

View File

@ -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) {

View File

@ -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>()

View File

@ -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 {

View File

@ -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())
}
}

View File

@ -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
View 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
}

View File

@ -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

View File

@ -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

View File

@ -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")