mirror of
https://github.com/bolucat/Archive.git
synced 2026-04-22 16:07:49 +08:00
Update On Fri Apr 10 21:07:38 CEST 2026
This commit is contained in:
@@ -1325,3 +1325,4 @@ Update On Mon Apr 6 21:13:19 CEST 2026
|
||||
Update On Tue Apr 7 21:18:54 CEST 2026
|
||||
Update On Wed Apr 8 21:30:11 CEST 2026
|
||||
Update On Thu Apr 9 21:21:01 CEST 2026
|
||||
Update On Fri Apr 10 21:07:29 CEST 2026
|
||||
|
||||
@@ -2,9 +2,11 @@ package com.github.kr328.clash
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import com.github.kr328.clash.common.util.componentName
|
||||
import com.github.kr328.clash.design.AppSettingsDesign
|
||||
import com.github.kr328.clash.design.model.Behavior
|
||||
import com.github.kr328.clash.design.store.UiStore.Companion.mainActivityAlias
|
||||
import com.github.kr328.clash.service.store.ServiceStore
|
||||
import com.github.kr328.clash.util.ApplicationObserver
|
||||
import kotlinx.coroutines.isActive
|
||||
@@ -69,9 +71,13 @@ class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), Behavior {
|
||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
||||
}
|
||||
packageManager.setComponentEnabledSetting(
|
||||
ComponentName(this, mainActivityAlias),
|
||||
mainActivityAlias,
|
||||
newState,
|
||||
PackageManager.DONT_KILL_APP
|
||||
)
|
||||
if (hide) {
|
||||
// Prevent launcher activity not found.
|
||||
ShortcutManagerCompat.removeAllDynamicShortcuts(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,5 +159,3 @@ class MainActivity : BaseActivity<MainDesign>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val mainActivityAlias = "${MainActivity::class.java.name}Alias"
|
||||
@@ -10,6 +10,7 @@ import com.github.kr328.clash.common.Global
|
||||
import com.github.kr328.clash.common.compat.currentProcessName
|
||||
import com.github.kr328.clash.common.constants.Intents
|
||||
import com.github.kr328.clash.common.log.Log
|
||||
import com.github.kr328.clash.design.store.UiStore
|
||||
import com.github.kr328.clash.remote.Remote
|
||||
import com.github.kr328.clash.service.util.sendServiceRecreated
|
||||
import com.github.kr328.clash.util.clashDir
|
||||
@@ -20,6 +21,8 @@ import com.github.kr328.clash.design.R as DesignR
|
||||
|
||||
@Suppress("unused")
|
||||
class MainApplication : Application() {
|
||||
private val uiStore by lazy(LazyThreadSafetyMode.NONE) { UiStore(this) }
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
super.attachBaseContext(base)
|
||||
|
||||
@@ -43,6 +46,12 @@ class MainApplication : Application() {
|
||||
}
|
||||
|
||||
private fun setupShortcuts() {
|
||||
if (uiStore.hideAppIcon) {
|
||||
// Prevent launcher activity not found.
|
||||
ShortcutManagerCompat.removeAllDynamicShortcuts(this)
|
||||
return
|
||||
}
|
||||
|
||||
val icon = IconCompat.createWithResource(this, R.mipmap.ic_launcher)
|
||||
val flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or
|
||||
|
||||
+10
-1
@@ -1,6 +1,8 @@
|
||||
package com.github.kr328.clash.design.store
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import com.github.kr328.clash.common.store.Store
|
||||
import com.github.kr328.clash.common.store.asStoreProvider
|
||||
import com.github.kr328.clash.core.model.ProxySort
|
||||
@@ -27,7 +29,11 @@ class UiStore(context: Context) {
|
||||
|
||||
var hideAppIcon: Boolean by store.boolean(
|
||||
key = "hide_app_icon",
|
||||
defaultValue = false
|
||||
defaultValue = context.packageManager.getComponentEnabledSetting(context.mainActivityAlias)
|
||||
.let { state ->
|
||||
state != PackageManager.COMPONENT_ENABLED_STATE_ENABLED &&
|
||||
state != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||
},
|
||||
)
|
||||
|
||||
var hideFromRecents: Boolean by store.boolean(
|
||||
@@ -74,5 +80,8 @@ class UiStore(context: Context) {
|
||||
|
||||
companion object {
|
||||
private const val PREFERENCE_NAME = "ui"
|
||||
|
||||
val Context.mainActivityAlias: ComponentName
|
||||
get() = ComponentName(this, "com.github.kr328.clash.MainActivityAlias")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
"github.com/metacubex/mihomo/transport/masque"
|
||||
"github.com/metacubex/mihomo/transport/tuic/common"
|
||||
|
||||
connectip "github.com/metacubex/connect-ip-go"
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/quic-go"
|
||||
wireguard "github.com/metacubex/sing-wireguard"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
@@ -34,11 +35,12 @@ import (
|
||||
|
||||
type Masque struct {
|
||||
*Base
|
||||
tlsConfig *tls.Config
|
||||
quicConfig *quic.Config
|
||||
tunDevice wireguard.Device
|
||||
resolver resolver.Resolver
|
||||
uri string
|
||||
tlsConfig *tls.Config
|
||||
quicConfig *quic.Config
|
||||
tunDevice wireguard.Device
|
||||
resolver resolver.Resolver
|
||||
uri string
|
||||
h2Transport *http.Http2Transport
|
||||
|
||||
runCtx context.Context
|
||||
runCancel context.CancelFunc
|
||||
@@ -51,17 +53,19 @@ type Masque struct {
|
||||
|
||||
type MasqueOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
PrivateKey string `proxy:"private-key"`
|
||||
PublicKey string `proxy:"public-key"`
|
||||
Ip string `proxy:"ip,omitempty"`
|
||||
Ipv6 string `proxy:"ipv6,omitempty"`
|
||||
URI string `proxy:"uri,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
MTU int `proxy:"mtu,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
PrivateKey string `proxy:"private-key"`
|
||||
PublicKey string `proxy:"public-key"`
|
||||
Ip string `proxy:"ip,omitempty"`
|
||||
Ipv6 string `proxy:"ipv6,omitempty"`
|
||||
URI string `proxy:"uri,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
MTU int `proxy:"mtu,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
|
||||
CongestionController string `proxy:"congestion-controller,omitempty"`
|
||||
CWND int `proxy:"cwnd,omitempty"`
|
||||
@@ -150,12 +154,26 @@ func NewMasque(option MasqueOption) (*Masque, error) {
|
||||
sni = masque.ConnectSNI
|
||||
}
|
||||
|
||||
tlsConfig, err := masque.PrepareTlsConfig(privKey, ecPubKey, sni)
|
||||
tlsConfig, err := masque.PrepareTlsConfig(privKey, ecPubKey, sni, option.SkipCertVerify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare TLS config: %v\n", err)
|
||||
}
|
||||
outbound.tlsConfig = tlsConfig
|
||||
|
||||
if option.Network == "h2" {
|
||||
tlsConfig.NextProtos = []string{"h2"}
|
||||
outbound.h2Transport = &http.Http2Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
c, err := outbound.dialer.DialContext(ctx, "tcp", outbound.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.Client(c, tlsConfig), nil
|
||||
},
|
||||
ReadIdleTimeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
outbound.quicConfig = &quic.Config{
|
||||
EnableDatagrams: true,
|
||||
InitialPacketSize: 1242,
|
||||
@@ -229,27 +247,43 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
w.runDevice.Store(true)
|
||||
}
|
||||
|
||||
udpAddr, err := resolveUDPAddr(ctx, "udp", w.addr, w.prefer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pc net.PacketConn
|
||||
var closer io.Closer
|
||||
var ipConn masque.IpConn
|
||||
var err error
|
||||
if w.h2Transport != nil {
|
||||
closer, ipConn, err = masque.ConnectTunnelH2(ctx, w.h2Transport, w.uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var udpAddr *net.UDPAddr
|
||||
udpAddr, err = resolveUDPAddr(ctx, "udp", w.addr, w.prefer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pc, err := w.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pc, err = w.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quicConn, err := quic.Dial(ctx, pc, udpAddr, w.tlsConfig, w.quicConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport := quic.Transport{Conn: pc}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.Dial(ctx, udpAddr, w.tlsConfig, w.quicConfig)
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
|
||||
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
|
||||
|
||||
tr, ipConn, err := masque.ConnectTunnel(ctx, quicConn, w.uri)
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
return err
|
||||
closer, ipConn, err = masque.ConnectTunnel(ctx, quicConn, w.uri)
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.running.Store(true)
|
||||
@@ -258,8 +292,10 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
contextutils.AfterFunc(runCtx, func() {
|
||||
w.running.Store(false)
|
||||
_ = ipConn.Close()
|
||||
_ = tr.Close()
|
||||
_ = pc.Close()
|
||||
_ = closer.Close()
|
||||
if pc != nil {
|
||||
_ = pc.Close()
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
@@ -276,7 +312,7 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
}
|
||||
icmp, err := ipConn.WritePacket(buf[:sizes[0]])
|
||||
if err != nil {
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err)
|
||||
return
|
||||
}
|
||||
@@ -299,7 +335,7 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
for runCtx.Err() == nil {
|
||||
n, err := ipConn.ReadPacket(buf)
|
||||
if err != nil {
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -600,7 +600,10 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, packetConn, udpAddr, tlsConfig, cfg)
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.DialEarly(ctx, udpAddr, tlsConfig, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
@@ -752,7 +755,10 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, packetConn, udpAddr, tlsConfig, cfg)
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.DialEarly(ctx, udpAddr, tlsConfig, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
|
||||
@@ -554,11 +554,11 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.
|
||||
IP: net.ParseIP(ip),
|
||||
Port: portInt,
|
||||
}
|
||||
conn, err := doh.dialer.ListenPacket(ctx, "udp", addr)
|
||||
packetConn, err := doh.dialer.ListenPacket(ctx, "udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport := quic.Transport{Conn: conn}
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
tlsCfg = tlsCfg.Clone()
|
||||
@@ -568,7 +568,12 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.
|
||||
// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
|
||||
tlsCfg.ServerName = doh.url.Host
|
||||
}
|
||||
return transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg)
|
||||
quicConn, err := transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return quicConn, nil
|
||||
}
|
||||
|
||||
// probeH3 runs a test to check whether QUIC is faster than TLS for this
|
||||
|
||||
@@ -279,7 +279,7 @@ func (doq *dnsOverQUIC) openStream(ctx context.Context, conn *quic.Conn) (*quic.
|
||||
}
|
||||
|
||||
// openConnection opens a new QUIC connection.
|
||||
func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, err error) {
|
||||
func (doq *dnsOverQUIC) openConnection(ctx context.Context) (quicConn *quic.Conn, err error) {
|
||||
// we're using bootstrapped address instead of what's passed to the function
|
||||
// it does not create an actual connection, but it helps us determine
|
||||
// what IP is actually reachable (when there're v4/v6 addresses).
|
||||
@@ -298,7 +298,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, er
|
||||
|
||||
p, err := strconv.Atoi(port)
|
||||
udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p}
|
||||
udp, err := doq.dialer.ListenPacket(ctx, "udp", addr)
|
||||
packetConn, err := doq.dialer.ListenPacket(ctx, "udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -322,15 +322,16 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := quic.Transport{Conn: udp}
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
conn, err = transport.Dial(ctx, &udpAddr, tlsConfig, doq.getQUICConfig())
|
||||
quicConn, err = transport.Dial(ctx, &udpAddr, tlsConfig, doq.getQUICConfig())
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
return quicConn, nil
|
||||
}
|
||||
|
||||
// closeConnWithError closes the active connection with error to make sure that
|
||||
|
||||
@@ -1080,6 +1080,23 @@ proxies: # socks5
|
||||
# dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效
|
||||
# congestion-controller: bbr # 默认不开启
|
||||
|
||||
# masque-h2
|
||||
- name: "masque-h2"
|
||||
type: masque
|
||||
server: 162.159.198.2
|
||||
port: 443
|
||||
private-key: MHcCAQEEILI1eOtnbEIh89Fj4yNDuFR6UjayCKI3NdLl3DhetimWoAoGCCqGSM49AwEHoUQDQgAEgyXrE8v+hHsHy3ewSb3WcRjYgCrM9T9hiE0Uv6k2DZ1+4kefrDT9v1Q/8wdRigTf6t6gGNUV8W+IUMdrfUt+9g==
|
||||
public-key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIaU7MToJm9NKp8YfGxR6r+/h4mcG7SxI8tsW8OR1A5tv/zCzVbCRRh2t87/kxnP6lAy0lkr7qYwu+ox+k3dr6w==
|
||||
ip: 172.16.0.2
|
||||
ipv6: 2606:4700:110:84c0:163a:4914:a0ad:3342
|
||||
mtu: 1280
|
||||
udp: true
|
||||
network: h2
|
||||
# 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接
|
||||
# dialer-proxy: "ss1"
|
||||
# remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false
|
||||
# dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效
|
||||
|
||||
# tuic
|
||||
- name: tuic
|
||||
server: www.example.com
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/enfein/mieru/v3 v3.30.0
|
||||
github.com/enfein/mieru/v3 v3.30.1
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang/snappy v1.0.0
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
|
||||
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||
github.com/enfein/mieru/v3 v3.30.0 h1:g7v0TuK7y0ZMn6TOdjOs8WEUQk8bvs6WYPBJ16SKdBU=
|
||||
github.com/enfein/mieru/v3 v3.30.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.30.1 h1:gHHXQfpQO/5d789o9kokVfej7jl795aJwPihUk3gTDU=
|
||||
github.com/enfein/mieru/v3 v3.30.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
package masque
|
||||
|
||||
// copy and modify from: https://github.com/Diniboy1123/connect-ip-go/blob/8d7bb0a858a2674046a7cb5538749e4c826c3538/client_h2.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/metacubex/mihomo/common/contextutils"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/quic-go/quicvarint"
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
)
|
||||
|
||||
const h2DatagramCapsuleType uint64 = 0
|
||||
|
||||
const (
|
||||
ipv4HeaderLen = 20
|
||||
ipv6HeaderLen = 40
|
||||
)
|
||||
|
||||
func ConnectTunnelH2(ctx context.Context, h2Transport *http.Http2Transport, connectUri string) (*http.Http2ClientConn, IpConn, error) {
|
||||
additionalHeaders := http.Header{
|
||||
"User-Agent": []string{""},
|
||||
}
|
||||
template := uritemplate.MustNew(connectUri)
|
||||
|
||||
h2Headers := additionalHeaders.Clone()
|
||||
h2Headers.Set("cf-connect-proto", "cf-connect-ip")
|
||||
// TODO: support PQC
|
||||
h2Headers.Set("pq-enabled", "false")
|
||||
|
||||
conn, err := h2Transport.DialTLSContext(ctx, "tcp", ":0", nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to dial: %w", err)
|
||||
}
|
||||
|
||||
cc, err := h2Transport.NewClientConn(conn)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to create client connection: %w", err)
|
||||
}
|
||||
|
||||
if !cc.ReserveNewRequest() {
|
||||
_ = cc.Close()
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to reserve client connection: %w", err)
|
||||
}
|
||||
|
||||
ipConn, rsp, err := dialH2(ctx, cc, template, h2Headers)
|
||||
if err != nil {
|
||||
_ = cc.Close()
|
||||
if strings.Contains(err.Error(), "tls: access denied") {
|
||||
return nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service")
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to dial connect-ip over HTTP/2: %w", err)
|
||||
}
|
||||
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
_ = ipConn.Close()
|
||||
_ = cc.Close()
|
||||
return nil, nil, fmt.Errorf("failed to dial connect-ip: %v", rsp.Status)
|
||||
}
|
||||
|
||||
return cc, ipConn, nil
|
||||
}
|
||||
|
||||
// dialH2 dials a proxied connection over HTTP/2 CONNECT-IP.
|
||||
//
|
||||
// This transport carries proxied packets inside HTTP capsule DATAGRAM frames.
|
||||
func dialH2(ctx context.Context, rt http.RoundTripper, template *uritemplate.Template, additionalHeaders http.Header) (*h2IpConn, *http.Response, error) {
|
||||
if len(template.Varnames()) > 0 {
|
||||
return nil, nil, errors.New("connect-ip: IP flow forwarding not supported")
|
||||
}
|
||||
|
||||
u, err := url.Parse(template.Raw())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to parse URI: %w", err)
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithCancel(context.Background()) // reqCtx must disconnect from ctx, otherwise ctx would close the entire HTTP/2 connection.
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodConnect, u.String(), pr)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to create request: %w", err)
|
||||
}
|
||||
req.Host = authorityFromURL(u)
|
||||
req.ContentLength = -1
|
||||
req.Header = make(http.Header)
|
||||
for k, v := range additionalHeaders {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
stop := contextutils.AfterFunc(ctx, cancel) // temporarily connect ctx with reqCtx when client.Do
|
||||
rsp, err := rt.RoundTrip(req)
|
||||
stop() // disconnect ctx with reqCtx after client.Do
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to send request: %w", err)
|
||||
}
|
||||
if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
|
||||
cancel()
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
_ = rsp.Body.Close()
|
||||
return nil, rsp, fmt.Errorf("connect-ip: server responded with %d", rsp.StatusCode)
|
||||
}
|
||||
|
||||
stream := &h2DatagramStream{
|
||||
requestBody: pw,
|
||||
responseBody: rsp.Body,
|
||||
cancel: cancel,
|
||||
}
|
||||
return &h2IpConn{
|
||||
str: stream,
|
||||
closeChan: make(chan struct{}),
|
||||
}, rsp, nil
|
||||
}
|
||||
|
||||
func authorityFromURL(u *url.URL) string {
|
||||
if u.Port() != "" {
|
||||
return u.Host
|
||||
}
|
||||
host := u.Hostname()
|
||||
if host == "" {
|
||||
return u.Host
|
||||
}
|
||||
return host + ":443"
|
||||
}
|
||||
|
||||
type h2IpConn struct {
|
||||
str *h2DatagramStream
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
closeChan chan struct{}
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (c *h2IpConn) ReadPacket(b []byte) (n int, err error) {
|
||||
start:
|
||||
data, err := c.str.ReceiveDatagram(context.Background())
|
||||
if err != nil {
|
||||
defer func() {
|
||||
// There are no errors that can be recovered in h2 mode,
|
||||
// so calling Close allows the outer read loop to exit in the next iteration by returning net.ErrClosed.
|
||||
_ = c.Close()
|
||||
}()
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0, c.closeErr
|
||||
default:
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err := c.handleIncomingProxiedPacket(data); err != nil {
|
||||
log.Debugln("dropping proxied packet: %s", err)
|
||||
goto start
|
||||
}
|
||||
return copy(b, data), nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) handleIncomingProxiedPacket(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return errors.New("connect-ip: empty packet")
|
||||
}
|
||||
switch v := ipVersion(data); v {
|
||||
default:
|
||||
return fmt.Errorf("connect-ip: unknown IP versions: %d", v)
|
||||
case 4:
|
||||
if len(data) < ipv4HeaderLen {
|
||||
return fmt.Errorf("connect-ip: malformed datagram: too short")
|
||||
}
|
||||
case 6:
|
||||
if len(data) < ipv6HeaderLen {
|
||||
return fmt.Errorf("connect-ip: malformed datagram: too short")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WritePacket writes an IP packet to the stream.
|
||||
// If sending the packet fails, it might return an ICMP packet.
|
||||
// It is the caller's responsibility to send the ICMP packet to the sender.
|
||||
func (c *h2IpConn) WritePacket(b []byte) (icmp []byte, err error) {
|
||||
data, err := c.composeDatagram(b)
|
||||
if err != nil {
|
||||
log.Debugln("dropping proxied packet (%d bytes) that can't be proxied: %s", len(b), err)
|
||||
return nil, nil
|
||||
}
|
||||
if err := c.str.SendDatagram(data); err != nil {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return nil, c.closeErr
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) composeDatagram(b []byte) ([]byte, error) {
|
||||
// TODO: implement src, dst and ipproto checks
|
||||
if len(b) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
switch v := ipVersion(b); v {
|
||||
default:
|
||||
return nil, fmt.Errorf("connect-ip: unknown IP versions: %d", v)
|
||||
case 4:
|
||||
if len(b) < ipv4HeaderLen {
|
||||
return nil, fmt.Errorf("connect-ip: IPv4 packet too short")
|
||||
}
|
||||
ttl := b[8]
|
||||
if ttl <= 1 {
|
||||
return nil, fmt.Errorf("connect-ip: datagram TTL too small: %d", ttl)
|
||||
}
|
||||
b[8]-- // decrement TTL
|
||||
// recalculate the checksum
|
||||
binary.BigEndian.PutUint16(b[10:12], calculateIPv4Checksum(([ipv4HeaderLen]byte)(b[:ipv4HeaderLen])))
|
||||
case 6:
|
||||
if len(b) < ipv6HeaderLen {
|
||||
return nil, fmt.Errorf("connect-ip: IPv6 packet too short")
|
||||
}
|
||||
hopLimit := b[7]
|
||||
if hopLimit <= 1 {
|
||||
return nil, fmt.Errorf("connect-ip: datagram Hop Limit too small: %d", hopLimit)
|
||||
}
|
||||
b[7]-- // Decrement Hop Limit
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.closeErr == nil {
|
||||
c.closeErr = net.ErrClosed
|
||||
close(c.closeChan)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
err := c.str.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func ipVersion(b []byte) uint8 { return b[0] >> 4 }
|
||||
|
||||
func calculateIPv4Checksum(header [ipv4HeaderLen]byte) uint16 {
|
||||
// add every 16-bit word in the header, skipping the checksum field (bytes 10 and 11)
|
||||
var sum uint32
|
||||
for i := 0; i < len(header); i += 2 {
|
||||
if i == 10 {
|
||||
continue // skip checksum field
|
||||
}
|
||||
sum += uint32(binary.BigEndian.Uint16(header[i : i+2]))
|
||||
}
|
||||
for (sum >> 16) > 0 {
|
||||
sum = (sum & 0xffff) + (sum >> 16)
|
||||
}
|
||||
return ^uint16(sum)
|
||||
}
|
||||
|
||||
type h2DatagramStream struct {
|
||||
requestBody *io.PipeWriter
|
||||
responseBody io.ReadCloser
|
||||
cancel context.CancelFunc
|
||||
|
||||
readMu sync.Mutex
|
||||
writeMu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *h2DatagramStream) ReceiveDatagram(_ context.Context) ([]byte, error) {
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
|
||||
reader := quicvarint.NewReader(s.responseBody)
|
||||
for {
|
||||
capsuleType, err := quicvarint.Read(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen, err := quicvarint.Read(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := make([]byte, payloadLen)
|
||||
_, err = io.ReadFull(reader, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if capsuleType != h2DatagramCapsuleType {
|
||||
continue
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *h2DatagramStream) SendDatagram(data []byte) error {
|
||||
frame := make([]byte, 0, quicvarint.Len(h2DatagramCapsuleType)+quicvarint.Len(uint64(len(data)))+len(data))
|
||||
frame = quicvarint.Append(frame, h2DatagramCapsuleType)
|
||||
frame = quicvarint.Append(frame, uint64(len(data)))
|
||||
frame = append(frame, data...)
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
_, err := s.requestBody.Write(frame)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect-ip: failed to send datagram capsule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *h2DatagramStream) Close() error {
|
||||
_ = s.requestBody.Close()
|
||||
err := s.responseBody.Close()
|
||||
s.cancel()
|
||||
return err
|
||||
}
|
||||
@@ -27,9 +27,15 @@ const (
|
||||
ConnectURI = "https://cloudflareaccess.com"
|
||||
)
|
||||
|
||||
type IpConn interface {
|
||||
ReadPacket(b []byte) (n int, err error)
|
||||
WritePacket(b []byte) (icmp []byte, err error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// PrepareTlsConfig creates a TLS configuration using the provided certificate and SNI (Server Name Indication).
|
||||
// It also verifies the peer's public key against the provided public key.
|
||||
func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sni string) (*tls.Config, error) {
|
||||
func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sni string, insecure bool) (*tls.Config, error) {
|
||||
verfiyCert := func(cert *x509.Certificate) error {
|
||||
if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok {
|
||||
// we only support ECDSA
|
||||
@@ -77,6 +83,9 @@ func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sn
|
||||
return err
|
||||
},
|
||||
}
|
||||
if insecure {
|
||||
tlsConfig.VerifyConnection = nil
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlN
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, packetConn, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.DialEarly(ctx, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
|
||||
Generated
+78
-72
@@ -379,7 +379,7 @@ dependencies = [
|
||||
"objc2-foundation 0.3.2",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
"wl-clipboard-rs",
|
||||
"x11rb",
|
||||
]
|
||||
@@ -1026,7 +1026,7 @@ dependencies = [
|
||||
"boa_interner",
|
||||
"boa_macros",
|
||||
"boa_string",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"num-bigint",
|
||||
"rustc-hash 2.1.2",
|
||||
]
|
||||
@@ -1058,7 +1058,7 @@ dependencies = [
|
||||
"futures-lite",
|
||||
"hashbrown 0.16.1",
|
||||
"icu_normalizer",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"intrusive-collections",
|
||||
"itertools 0.14.0",
|
||||
"num-bigint",
|
||||
@@ -1104,7 +1104,7 @@ dependencies = [
|
||||
"boa_gc",
|
||||
"boa_macros",
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"once_cell",
|
||||
"phf 0.13.1",
|
||||
"rustc-hash 2.1.2",
|
||||
@@ -1596,7 +1596,7 @@ dependencies = [
|
||||
"hex",
|
||||
"humansize",
|
||||
"image",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"md-5",
|
||||
@@ -1743,7 +1743,7 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2354,7 +2354,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users 0.5.2",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2883,7 +2883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3874,7 +3874,7 @@ dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"http",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"slab",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
@@ -3959,6 +3959,12 @@ dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
|
||||
[[package]]
|
||||
name = "heapless"
|
||||
version = "0.7.17"
|
||||
@@ -4415,12 +4421,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.13.1"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.17.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
@@ -4805,7 +4811,7 @@ checksum = "02cb977175687f33fa4afa0c95c112b987ea1443e5a51c8f8ff27dc618270cc2"
|
||||
dependencies = [
|
||||
"cssparser",
|
||||
"html5ever",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"selectors",
|
||||
]
|
||||
|
||||
@@ -4907,7 +4913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.48.5",
|
||||
"windows-targets 0.53.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5307,7 +5313,7 @@ dependencies = [
|
||||
"half",
|
||||
"hashbrown 0.16.1",
|
||||
"hexf-parse",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"libm",
|
||||
"log",
|
||||
"num-traits",
|
||||
@@ -5701,7 +5707,7 @@ dependencies = [
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"interprocess 2.3.0 (git+https://github.com/libnyanpasu/interprocess.git?branch=fix/linux-32bit-build)",
|
||||
"nyanpasu-utils",
|
||||
"pin-project-lite",
|
||||
@@ -6307,7 +6313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6341,9 +6347,9 @@ checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e"
|
||||
|
||||
[[package]]
|
||||
name = "oxc-miette"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60a7ba54c704edefead1f44e9ef09c43e5cfae666bdc33516b066011f0e6ebf7"
|
||||
checksum = "4356a61f2ed4c9b3610245215fbf48970eb277126919f87db9d0efa93a74245c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"owo-colors",
|
||||
@@ -6356,9 +6362,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc-miette-derive"
|
||||
version = "2.7.0"
|
||||
version = "2.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4faecb54d0971f948fbc1918df69b26007e6f279a204793669542e1e8b75eb3"
|
||||
checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -6367,9 +6373,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_allocator"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e6fc6ce99f6a28fd477c6df500bbc9bf1c39db166952e15bea218459cc0db0c"
|
||||
checksum = "7cce9493fc18c7f2b9274baba258555d88cc1fab3ac3c4b293433b4f85ad097b"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"hashbrown 0.16.1",
|
||||
@@ -6379,9 +6385,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49fa0813bf9fcff5a4e48fc186ee15a0d276b30b0b575389a34a530864567819"
|
||||
checksum = "29366258930c55e2578e231995d2079cba12793429454fa892f01d985821a554"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"oxc_allocator",
|
||||
@@ -6396,9 +6402,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast_macros"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a2b2a2e09ff0dd4790a5ceb4a93349e0ea769d4d98d778946de48decb763b18"
|
||||
checksum = "617bf2f55d04db8d6fea9583569c7e4d9052297f76f2f8ae31b1f4ef8bcfd98e"
|
||||
dependencies = [
|
||||
"phf 0.13.1",
|
||||
"proc-macro2",
|
||||
@@ -6408,9 +6414,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ast_visit"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef6d2304cb25dbbd028440591bf289ef16e3df98517930e79dcc304be64b3045"
|
||||
checksum = "4344952280d3e8cbfed93da2775c460bbded12f388404daa662dc0ee731e051f"
|
||||
dependencies = [
|
||||
"oxc_allocator",
|
||||
"oxc_ast",
|
||||
@@ -6420,15 +6426,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_data_structures"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e8f59bed9522098da177d894dc8635fb3eae218ff97d9c695900cb11fd10a2"
|
||||
checksum = "a3a309fcc491b31039bd2a77d8517278c198f566c284e9a18977dab801c05681"
|
||||
|
||||
[[package]]
|
||||
name = "oxc_diagnostics"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0476859d4319f2b063f7c4a3120ee5b7e3e48032865ca501f8545ff44badcff"
|
||||
checksum = "a1c0f18571aac10db23d1ab681108102ac735c50142c5418ec8272e1d861219f"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"oxc-miette",
|
||||
@@ -6437,9 +6443,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_ecmascript"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bcf46e5b1a6f8ea3797e887a9db4c79ed15894ca8685eb628da462d4c4e913f"
|
||||
checksum = "4eaddc891449b4c7d8720714d6939c99fc531054c8f7decba9ffdd1c70a7b67b"
|
||||
dependencies = [
|
||||
"cow-utils",
|
||||
"num-bigint",
|
||||
@@ -6453,9 +6459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_estree"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2251e6b61eab7b96f0e9d140b68b0f0d8a851c7d260725433e18b1babdcb9430"
|
||||
checksum = "8dd0f39cc6f2014fc1a60a563903c6c6c88f856772d44f390fe876a575bd7c97"
|
||||
|
||||
[[package]]
|
||||
name = "oxc_index"
|
||||
@@ -6469,9 +6475,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_parser"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439d2580047b77faf6e60d358b48e5292e0e026b9cfc158d46ddd0175244bb26"
|
||||
checksum = "ecf347b9ba5fd251f215f0c44602fbec98c01ea4cf13ae2682167f33d8a8d0b4"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cow-utils",
|
||||
@@ -6492,9 +6498,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_regular_expression"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fb5669d3298a92d440afec516943745794cb4cf977911728cd73e3438db87b9"
|
||||
checksum = "922016d2def4d0a2b17c907bda16d6eb20516622ae818eb8662f69b353ba9f20"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"oxc_allocator",
|
||||
@@ -6508,9 +6514,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_span"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1d452f6a664627bdd0f1f1586f9258f81cd7edc5c83e9ef50019f701ef1722d"
|
||||
checksum = "9b4413a552b443c777dd2782bc49e719a20cb36434c9b196e9021259028ca74c"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"oxc-miette",
|
||||
@@ -6522,9 +6528,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_str"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7a27c4371f69387f3d6f8fa56f70e4c6fa6aedc399285de6ec02bb9fd148d7"
|
||||
checksum = "321abe830f84ab9c13ac43eadb625f7e8ccddab6a150732d1e5bf9dde043ef4f"
|
||||
dependencies = [
|
||||
"compact_str",
|
||||
"hashbrown 0.16.1",
|
||||
@@ -6534,9 +6540,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "oxc_syntax"
|
||||
version = "0.123.0"
|
||||
version = "0.124.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d60d91023aafc256ab99c3dbf6181473e495695029c0152d2093e87df18ffe2"
|
||||
checksum = "11919498c468e21e0688d6c99e37c15f2825a64cfa2f9a0c99d6f767076011d8"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"cow-utils",
|
||||
@@ -6702,7 +6708,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db"
|
||||
dependencies = [
|
||||
"fixedbitset 0.4.2",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6948,7 +6954,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"quick-xml 0.38.3",
|
||||
"serde",
|
||||
"time",
|
||||
@@ -7289,7 +7295,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7932,7 +7938,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7945,7 +7951,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8003,7 +8009,7 @@ dependencies = [
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8317,7 +8323,7 @@ version = "1.0.149"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
@@ -8386,7 +8392,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"hex",
|
||||
"indexmap 1.9.3",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"schemars 0.9.0",
|
||||
"schemars 1.0.4",
|
||||
"serde",
|
||||
@@ -8414,7 +8420,7 @@ version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -8426,7 +8432,7 @@ name = "serde_yaml_ng"
|
||||
version = "0.10.0"
|
||||
source = "git+https://github.com/libnyanpasu/serde-yaml-ng.git?branch=feat/specta-update#363da138dc97f90ef0a356f971c2dc1d8fe04c9e"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
@@ -8833,7 +8839,7 @@ version = "2.0.0-rc.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab7f01e9310a820edd31c80fde3cae445295adde21a3f9416517d7d65015b971"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"paste",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -9693,7 +9699,7 @@ dependencies = [
|
||||
"getrandom 0.4.1",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10065,7 +10071,7 @@ version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"serde",
|
||||
"serde_spanned 1.0.0",
|
||||
"toml_datetime 0.7.0",
|
||||
@@ -10098,7 +10104,7 @@ version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
@@ -10109,7 +10115,7 @@ version = "0.20.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"toml_datetime 0.6.11",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
@@ -10120,7 +10126,7 @@ version = "0.22.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||
dependencies = [
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"serde",
|
||||
"serde_spanned 0.6.9",
|
||||
"toml_datetime 0.6.11",
|
||||
@@ -10929,7 +10935,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
@@ -10968,7 +10974,7 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"semver 1.0.28",
|
||||
]
|
||||
|
||||
@@ -11299,7 +11305,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"document-features",
|
||||
"hashbrown 0.16.1",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"naga",
|
||||
"once_cell",
|
||||
@@ -11485,7 +11491,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12274,7 +12280,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck 0.5.0",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"prettyplease",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
@@ -12305,7 +12311,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.10.0",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -12324,7 +12330,7 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"log",
|
||||
"semver 1.0.28",
|
||||
"serde",
|
||||
@@ -12744,7 +12750,7 @@ checksum = "caa8cd6af31c3b31c6631b8f483848b91589021b28fffe50adada48d4f4d2ed1"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"crc32fast",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"memchr",
|
||||
]
|
||||
|
||||
@@ -12762,7 +12768,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"getrandom 0.4.1",
|
||||
"hmac",
|
||||
"indexmap 2.13.1",
|
||||
"indexmap 2.14.0",
|
||||
"lzma-rust2",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
|
||||
@@ -169,12 +169,12 @@ display-info = "0.5.0" # should be removed after upgrading to tauri v2
|
||||
|
||||
# OXC (The Oxidation Compiler)
|
||||
# We use it to parse and transpile the old script profile to esm based script profile
|
||||
oxc_parser = "0.123"
|
||||
oxc_allocator = "0.123"
|
||||
oxc_span = "0.123"
|
||||
oxc_ast = "0.123"
|
||||
oxc_syntax = "0.123"
|
||||
oxc_ast_visit = "0.123"
|
||||
oxc_parser = "0.124"
|
||||
oxc_allocator = "0.124"
|
||||
oxc_span = "0.124"
|
||||
oxc_ast = "0.124"
|
||||
oxc_syntax = "0.124"
|
||||
oxc_ast_visit = "0.124"
|
||||
|
||||
# Lua Integration
|
||||
mlua = { version = "0.11", features = [
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-query": "5.96.2",
|
||||
"@tanstack/react-query": "5.97.0",
|
||||
"@tauri-apps/api": "2.10.1",
|
||||
"ahooks": "3.9.7",
|
||||
"dayjs": "1.11.20",
|
||||
|
||||
@@ -70,9 +70,9 @@
|
||||
"@csstools/normalize.css": "12.1.1",
|
||||
"@emotion/babel-plugin": "11.13.5",
|
||||
"@emotion/react": "11.14.0",
|
||||
"@iconify/json": "2.2.460",
|
||||
"@iconify/json": "2.2.461",
|
||||
"@monaco-editor/react": "4.7.0",
|
||||
"@tanstack/react-query": "5.96.2",
|
||||
"@tanstack/react-query": "5.97.0",
|
||||
"@tanstack/react-router": "1.168.10",
|
||||
"@tanstack/react-router-devtools": "1.166.11",
|
||||
"@tanstack/router-plugin": "1.167.12",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"manifest_version": 1,
|
||||
"latest": {
|
||||
"mihomo": "v1.19.23",
|
||||
"mihomo_alpha": "alpha-d801e6b",
|
||||
"mihomo_alpha": "alpha-d9c447a",
|
||||
"clash_rs": "v0.9.6",
|
||||
"clash_premium": "2023-09-05-gdcc8d87",
|
||||
"clash_rs_alpha": "0.9.6-alpha+sha.415b05e"
|
||||
@@ -69,5 +69,5 @@
|
||||
"linux-armv7hf": "clash-rs-armv7-unknown-linux-gnueabihf"
|
||||
}
|
||||
},
|
||||
"updated_at": "2026-04-08T22:25:38.710Z"
|
||||
"updated_at": "2026-04-09T22:26:35.545Z"
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
"postcss-html": "1.8.1",
|
||||
"postcss-import": "16.1.1",
|
||||
"postcss-scss": "4.0.9",
|
||||
"prettier": "3.8.1",
|
||||
"prettier": "3.8.2",
|
||||
"prettier-plugin-ember-template-tag": "2.1.4",
|
||||
"prettier-plugin-tailwindcss": "0.7.2",
|
||||
"prettier-plugin-toml": "2.0.6",
|
||||
|
||||
Generated
+43
-43
@@ -30,7 +30,7 @@ importers:
|
||||
version: 20.5.0
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
specifier: 4.7.1
|
||||
version: 4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.1))(prettier@3.8.1)
|
||||
version: 4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2)
|
||||
'@tauri-apps/cli':
|
||||
specifier: 2.10.1
|
||||
version: 2.10.1
|
||||
@@ -83,17 +83,17 @@ importers:
|
||||
specifier: 4.0.9
|
||||
version: 4.0.9(postcss@8.5.9)
|
||||
prettier:
|
||||
specifier: 3.8.1
|
||||
version: 3.8.1
|
||||
specifier: 3.8.2
|
||||
version: 3.8.2
|
||||
prettier-plugin-ember-template-tag:
|
||||
specifier: 2.1.4
|
||||
version: 2.1.4(prettier@3.8.1)
|
||||
version: 2.1.4(prettier@3.8.2)
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: 0.7.2
|
||||
version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.1))(prettier@3.8.1))(@prettier/plugin-oxc@0.1.3)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1))(prettier@3.8.1)
|
||||
version: 0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2))(@prettier/plugin-oxc@0.1.3)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.2))(prettier@3.8.2)
|
||||
prettier-plugin-toml:
|
||||
specifier: 2.0.6
|
||||
version: 2.0.6(prettier@3.8.1)
|
||||
version: 2.0.6(prettier@3.8.2)
|
||||
stylelint:
|
||||
specifier: 17.6.0
|
||||
version: 17.6.0(typescript@5.9.3)
|
||||
@@ -128,8 +128,8 @@ importers:
|
||||
frontend/interface:
|
||||
dependencies:
|
||||
'@tanstack/react-query':
|
||||
specifier: 5.96.2
|
||||
version: 5.96.2(react@19.2.5)
|
||||
specifier: 5.97.0
|
||||
version: 5.97.0(react@19.2.5)
|
||||
'@tauri-apps/api':
|
||||
specifier: 2.10.1
|
||||
version: 2.10.1
|
||||
@@ -337,14 +337,14 @@ importers:
|
||||
specifier: 11.14.0
|
||||
version: 11.14.0(@types/react@19.2.14)(react@19.2.5)
|
||||
'@iconify/json':
|
||||
specifier: 2.2.460
|
||||
version: 2.2.460
|
||||
specifier: 2.2.461
|
||||
version: 2.2.461
|
||||
'@monaco-editor/react':
|
||||
specifier: 4.7.0
|
||||
version: 4.7.0(monaco-editor@0.55.1)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
'@tanstack/react-query':
|
||||
specifier: 5.96.2
|
||||
version: 5.96.2(react@19.2.5)
|
||||
specifier: 5.97.0
|
||||
version: 5.97.0(react@19.2.5)
|
||||
'@tanstack/react-router':
|
||||
specifier: 1.168.10
|
||||
version: 1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
|
||||
@@ -449,7 +449,7 @@ importers:
|
||||
version: 3.2.2(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite-plugin-sass-dts:
|
||||
specifier: 1.3.37
|
||||
version: 1.3.37(postcss@8.5.9)(prettier@3.8.1)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
version: 1.3.37(postcss@8.5.9)(prettier@3.8.2)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
vite-plugin-svgr:
|
||||
specifier: 4.5.0
|
||||
version: 4.5.0(rollup@4.46.2)(typescript@5.9.3)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2))
|
||||
@@ -1717,8 +1717,8 @@ packages:
|
||||
prettier-plugin-ember-template-tag:
|
||||
optional: true
|
||||
|
||||
'@iconify/json@2.2.460':
|
||||
resolution: {integrity: sha512-WVtlJMsLYp/s7SzIhTOgIOYMMlS6xJC1s6/jJz4Z8wP8RoxP1OaueT1Bg2YrxJ+BEjHlpvzG3RdirbjxdH8bvA==}
|
||||
'@iconify/json@2.2.461':
|
||||
resolution: {integrity: sha512-UiujJDldh6bD5v0EDbB6aKgye1tW3mteqmjHAwXi1bLQm9OYnHAWBIe8ogVZB2ttEFFraOBFkpe3VFwtxRSKSQ==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
@@ -3994,11 +3994,11 @@ packages:
|
||||
resolution: {integrity: sha512-Wo1iKt2b9OT7d+YGhvEPD3DXvPv2etTusIMhMUoG7fbhmxcXCtIjJDEygy91Y2JFlwGyjqiBPRozme7UD8hoqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tanstack/query-core@5.96.2':
|
||||
resolution: {integrity: sha512-hzI6cTVh4KNRk8UtoIBS7Lv9g6BnJPXvBKsvYH1aGWvv0347jT3BnSvztOE+kD76XGvZnRC/t6qdW1CaIfwCeA==}
|
||||
'@tanstack/query-core@5.97.0':
|
||||
resolution: {integrity: sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==}
|
||||
|
||||
'@tanstack/react-query@5.96.2':
|
||||
resolution: {integrity: sha512-sYyzzJT4G0g02azzJ8o55VFFV31XvFpdUpG+unxS0vSaYsJnSPKGoI6WdPwUucJL1wpgGfwfmntNX/Ub1uOViA==}
|
||||
'@tanstack/react-query@5.97.0':
|
||||
resolution: {integrity: sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
|
||||
@@ -6862,8 +6862,8 @@ packages:
|
||||
peerDependencies:
|
||||
prettier: ^3.0.3
|
||||
|
||||
prettier@3.8.1:
|
||||
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
|
||||
prettier@3.8.2:
|
||||
resolution: {integrity: sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==}
|
||||
engines: {node: '>=14'}
|
||||
hasBin: true
|
||||
|
||||
@@ -9472,22 +9472,22 @@ snapshots:
|
||||
'@standard-schema/utils': 0.3.0
|
||||
react-hook-form: 7.72.1(react@19.2.5)
|
||||
|
||||
'@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.1))(prettier@3.8.1)':
|
||||
'@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.29.0
|
||||
'@babel/parser': 7.29.0
|
||||
'@babel/traverse': 7.29.0
|
||||
'@babel/types': 7.29.0
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
semver: 7.7.3
|
||||
optionalDependencies:
|
||||
'@prettier/plugin-oxc': 0.1.3
|
||||
content-tag: 4.0.0
|
||||
prettier-plugin-ember-template-tag: 2.1.4(prettier@3.8.1)
|
||||
prettier-plugin-ember-template-tag: 2.1.4(prettier@3.8.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@iconify/json@2.2.460':
|
||||
'@iconify/json@2.2.461':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
pathe: 2.0.3
|
||||
@@ -11575,11 +11575,11 @@ snapshots:
|
||||
dependencies:
|
||||
remove-accents: 0.5.0
|
||||
|
||||
'@tanstack/query-core@5.96.2': {}
|
||||
'@tanstack/query-core@5.97.0': {}
|
||||
|
||||
'@tanstack/react-query@5.96.2(react@19.2.5)':
|
||||
'@tanstack/react-query@5.97.0(react@19.2.5)':
|
||||
dependencies:
|
||||
'@tanstack/query-core': 5.96.2
|
||||
'@tanstack/query-core': 5.97.0
|
||||
react: 19.2.5
|
||||
|
||||
'@tanstack/react-router-devtools@1.166.11(@tanstack/react-router@1.168.10(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@tanstack/router-core@1.168.9)(csstype@3.2.3)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)':
|
||||
@@ -11647,7 +11647,7 @@ snapshots:
|
||||
'@tanstack/router-core': 1.168.9
|
||||
'@tanstack/router-utils': 1.161.6
|
||||
'@tanstack/virtual-file-routes': 1.161.7
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
recast: 0.23.11
|
||||
source-map: 0.7.4
|
||||
tsx: 4.21.0
|
||||
@@ -11792,7 +11792,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@tauri-apps/api': 2.10.1
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1)':
|
||||
'@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.2)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.17.7
|
||||
'@babel/parser': 7.29.0
|
||||
@@ -11800,7 +11800,7 @@ snapshots:
|
||||
'@babel/types': 7.17.0
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
optional: true
|
||||
@@ -14221,7 +14221,7 @@ snapshots:
|
||||
monaco-types: 0.1.0
|
||||
monaco-worker-manager: 2.0.1(monaco-editor@0.55.1)
|
||||
path-browserify: 1.0.1
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
vscode-languageserver-textdocument: 1.0.12
|
||||
vscode-languageserver-types: 3.17.5
|
||||
vscode-uri: 3.0.8
|
||||
@@ -14625,28 +14625,28 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.1):
|
||||
prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2):
|
||||
dependencies:
|
||||
'@babel/traverse': 7.29.0
|
||||
content-tag: 4.0.0
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.1))(prettier@3.8.1))(@prettier/plugin-oxc@0.1.3)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.1))(prettier@3.8.1):
|
||||
prettier-plugin-tailwindcss@0.7.2(@ianvs/prettier-plugin-sort-imports@4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2))(@prettier/plugin-oxc@0.1.3)(@trivago/prettier-plugin-sort-imports@4.3.0(prettier@3.8.2))(prettier@3.8.2):
|
||||
dependencies:
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
optionalDependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports': 4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.1))(prettier@3.8.1)
|
||||
'@ianvs/prettier-plugin-sort-imports': 4.7.1(@prettier/plugin-oxc@0.1.3)(content-tag@4.0.0)(prettier-plugin-ember-template-tag@2.1.4(prettier@3.8.2))(prettier@3.8.2)
|
||||
'@prettier/plugin-oxc': 0.1.3
|
||||
'@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.8.1)
|
||||
'@trivago/prettier-plugin-sort-imports': 4.3.0(prettier@3.8.2)
|
||||
|
||||
prettier-plugin-toml@2.0.6(prettier@3.8.1):
|
||||
prettier-plugin-toml@2.0.6(prettier@3.8.2):
|
||||
dependencies:
|
||||
'@taplo/lib': 0.5.0
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
|
||||
prettier@3.8.1: {}
|
||||
prettier@3.8.2: {}
|
||||
|
||||
prop-types@15.8.1:
|
||||
dependencies:
|
||||
@@ -15848,11 +15848,11 @@ snapshots:
|
||||
pathe: 0.2.0
|
||||
vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
vite-plugin-sass-dts@1.3.37(postcss@8.5.9)(prettier@3.8.1)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
vite-plugin-sass-dts@1.3.37(postcss@8.5.9)(prettier@3.8.2)(sass-embedded@1.99.0)(vite@7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)):
|
||||
dependencies:
|
||||
postcss: 8.5.9
|
||||
postcss-js: 4.0.1(postcss@8.5.9)
|
||||
prettier: 3.8.1
|
||||
prettier: 3.8.2
|
||||
sass-embedded: 1.99.0
|
||||
vite: 7.3.2(@types/node@24.11.0)(jiti@2.6.1)(less@4.2.0)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.99.0)(stylus@0.62.0)(terser@5.36.0)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ PROJECT_NAME=$(shell basename "${ROOT}")
|
||||
# - pkg/version/current.go
|
||||
#
|
||||
# Use `tools/bump_version.sh` script to change all those files at one shot.
|
||||
VERSION="3.30.0"
|
||||
VERSION="3.30.1"
|
||||
|
||||
# With .ONESHELL, each recipe is executed in a single shell instance.
|
||||
# This allows `cd` to affect subsequent commands in the same recipe.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mieru
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mieru
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy client
|
||||
License: GPLv3+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mieru
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: arm64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mieru
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy client
|
||||
License: GPLv3+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mita
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mita
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy server
|
||||
License: GPLv3+
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Package: mita
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Section: net
|
||||
Priority: optional
|
||||
Architecture: arm64
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
Name: mita
|
||||
Version: 3.30.0
|
||||
Version: 3.30.1
|
||||
Release: 1%{?dist}
|
||||
Summary: Mieru proxy server
|
||||
License: GPLv3+
|
||||
|
||||
@@ -18,32 +18,32 @@ Or you can manually install and configure proxy server using the steps below.
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita_3.30.0_amd64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita_3.30.0_arm64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita-3.30.0-1.x86_64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita-3.30.0-1.aarch64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.aarch64.rpm
|
||||
```
|
||||
|
||||
## Install mita package
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
sudo dpkg -i mita_3.30.0_amd64.deb
|
||||
sudo dpkg -i mita_3.30.1_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
sudo dpkg -i mita_3.30.0_arm64.deb
|
||||
sudo dpkg -i mita_3.30.1_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
sudo rpm -Uvh --force mita-3.30.0-1.x86_64.rpm
|
||||
sudo rpm -Uvh --force mita-3.30.1-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
sudo rpm -Uvh --force mita-3.30.0-1.aarch64.rpm
|
||||
sudo rpm -Uvh --force mita-3.30.1-1.aarch64.rpm
|
||||
```
|
||||
|
||||
Those instructions can also be used to upgrade the version of mita software package.
|
||||
|
||||
@@ -18,32 +18,32 @@ sudo python3 setup.py --lang=zh
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita_3.30.0_amd64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita_3.30.0_arm64.deb
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita_3.30.1_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita-3.30.0-1.x86_64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.0/mita-3.30.0-1.aarch64.rpm
|
||||
curl -LSO https://github.com/enfein/mieru/releases/download/v3.30.1/mita-3.30.1-1.aarch64.rpm
|
||||
```
|
||||
|
||||
## 安装 mita 软件包
|
||||
|
||||
```sh
|
||||
# Debian / Ubuntu - X86_64
|
||||
sudo dpkg -i mita_3.30.0_amd64.deb
|
||||
sudo dpkg -i mita_3.30.1_amd64.deb
|
||||
|
||||
# Debian / Ubuntu - ARM 64
|
||||
sudo dpkg -i mita_3.30.0_arm64.deb
|
||||
sudo dpkg -i mita_3.30.1_arm64.deb
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - X86_64
|
||||
sudo rpm -Uvh --force mita-3.30.0-1.x86_64.rpm
|
||||
sudo rpm -Uvh --force mita-3.30.1-1.x86_64.rpm
|
||||
|
||||
# RedHat / CentOS / Rocky Linux - ARM 64
|
||||
sudo rpm -Uvh --force mita-3.30.0-1.aarch64.rpm
|
||||
sudo rpm -Uvh --force mita-3.30.1-1.aarch64.rpm
|
||||
```
|
||||
|
||||
上述指令也可以用来升级 mita 软件包的版本。
|
||||
|
||||
@@ -36,6 +36,8 @@ import (
|
||||
// - profile name is not empty
|
||||
// - user name is not empty
|
||||
// - user has either a password or a hashed password
|
||||
// - user name length is not greater than 64 bytes
|
||||
// - user password (if set) length is not greater than 64 bytes
|
||||
// - user has no quota
|
||||
// - it has at least 1 server, and for each server:
|
||||
// 1. the server has either IP address or domain name
|
||||
@@ -55,6 +57,12 @@ func ValidateClientConfigSingleProfile(profile *pb.ClientProfile) error {
|
||||
if user.GetPassword() == "" && user.GetHashedPassword() == "" {
|
||||
return fmt.Errorf("user password is not set")
|
||||
}
|
||||
if len(user.GetName()) > 64 {
|
||||
return fmt.Errorf("user name exceeds 64 bytes")
|
||||
}
|
||||
if user.GetPassword() != "" && len(user.GetPassword()) > 64 {
|
||||
return fmt.Errorf("user password exceeds 64 bytes")
|
||||
}
|
||||
if len(user.GetQuotas()) != 0 {
|
||||
return fmt.Errorf("user quota is not supported by proxy client")
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
// It validates:
|
||||
// - user name is not empty
|
||||
// - user has either a password or a hashed password
|
||||
// - user name length is not greater than 64 bytes
|
||||
// - user password (if set) length is not greater than 64 bytes
|
||||
// - for each quota:
|
||||
// 1. number of days is valid
|
||||
// 2. traffic volume in megabyte is valid
|
||||
@@ -36,6 +38,12 @@ func ValidateServerConfigSingleUser(user *pb.User) error {
|
||||
if user.GetPassword() == "" && user.GetHashedPassword() == "" {
|
||||
return fmt.Errorf("user password is not set")
|
||||
}
|
||||
if len(user.GetName()) > 64 {
|
||||
return fmt.Errorf("user name exceeds 64 bytes")
|
||||
}
|
||||
if user.GetPassword() != "" && len(user.GetPassword()) > 64 {
|
||||
return fmt.Errorf("user password exceeds 64 bytes")
|
||||
}
|
||||
for _, quota := range user.GetQuotas() {
|
||||
if quota.GetDays() <= 0 {
|
||||
return fmt.Errorf("quota: number of days %d is invalid", quota.GetDays())
|
||||
|
||||
@@ -297,6 +297,24 @@ func TestClientApplyReject(t *testing.T) {
|
||||
}(),
|
||||
wantErrString: "socks5 authentication user is not set",
|
||||
},
|
||||
{
|
||||
name: "user_name_too_long",
|
||||
config: func() *pb.ClientConfig {
|
||||
c := validConfig()
|
||||
c.Profiles[0].User.Name = proto.String(strings.Repeat("a", 65))
|
||||
return c
|
||||
}(),
|
||||
wantErrString: "user name exceeds 64 bytes",
|
||||
},
|
||||
{
|
||||
name: "user_password_too_long",
|
||||
config: func() *pb.ClientConfig {
|
||||
c := validConfig()
|
||||
c.Profiles[0].User.Password = proto.String(strings.Repeat("a", 65))
|
||||
return c
|
||||
}(),
|
||||
wantErrString: "user password exceeds 64 bytes",
|
||||
},
|
||||
{
|
||||
name: "user_has_quota",
|
||||
config: func() *pb.ClientConfig {
|
||||
|
||||
@@ -226,6 +226,24 @@ func TestServerApplyReject(t *testing.T) {
|
||||
}(),
|
||||
wantErrString: "user name is not set",
|
||||
},
|
||||
{
|
||||
name: "user_name_too_long",
|
||||
config: func() *pb.ServerConfig {
|
||||
c := validConfig()
|
||||
c.Users[0].Name = proto.String(strings.Repeat("a", 65))
|
||||
return c
|
||||
}(),
|
||||
wantErrString: "user name exceeds 64 bytes",
|
||||
},
|
||||
{
|
||||
name: "user_password_too_long",
|
||||
config: func() *pb.ServerConfig {
|
||||
c := validConfig()
|
||||
c.Users[0].Password = proto.String(strings.Repeat("a", 65))
|
||||
return c
|
||||
}(),
|
||||
wantErrString: "user password exceeds 64 bytes",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
|
||||
@@ -16,5 +16,5 @@
|
||||
package version
|
||||
|
||||
const (
|
||||
AppVersion = "3.30.0"
|
||||
AppVersion = "3.30.1"
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
"github.com/metacubex/mihomo/transport/masque"
|
||||
"github.com/metacubex/mihomo/transport/tuic/common"
|
||||
|
||||
connectip "github.com/metacubex/connect-ip-go"
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/quic-go"
|
||||
wireguard "github.com/metacubex/sing-wireguard"
|
||||
M "github.com/metacubex/sing/common/metadata"
|
||||
@@ -34,11 +35,12 @@ import (
|
||||
|
||||
type Masque struct {
|
||||
*Base
|
||||
tlsConfig *tls.Config
|
||||
quicConfig *quic.Config
|
||||
tunDevice wireguard.Device
|
||||
resolver resolver.Resolver
|
||||
uri string
|
||||
tlsConfig *tls.Config
|
||||
quicConfig *quic.Config
|
||||
tunDevice wireguard.Device
|
||||
resolver resolver.Resolver
|
||||
uri string
|
||||
h2Transport *http.Http2Transport
|
||||
|
||||
runCtx context.Context
|
||||
runCancel context.CancelFunc
|
||||
@@ -51,17 +53,19 @@ type Masque struct {
|
||||
|
||||
type MasqueOption struct {
|
||||
BasicOption
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
PrivateKey string `proxy:"private-key"`
|
||||
PublicKey string `proxy:"public-key"`
|
||||
Ip string `proxy:"ip,omitempty"`
|
||||
Ipv6 string `proxy:"ipv6,omitempty"`
|
||||
URI string `proxy:"uri,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
MTU int `proxy:"mtu,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
Name string `proxy:"name"`
|
||||
Server string `proxy:"server"`
|
||||
Port int `proxy:"port"`
|
||||
PrivateKey string `proxy:"private-key"`
|
||||
PublicKey string `proxy:"public-key"`
|
||||
Ip string `proxy:"ip,omitempty"`
|
||||
Ipv6 string `proxy:"ipv6,omitempty"`
|
||||
URI string `proxy:"uri,omitempty"`
|
||||
SNI string `proxy:"sni,omitempty"`
|
||||
MTU int `proxy:"mtu,omitempty"`
|
||||
UDP bool `proxy:"udp,omitempty"`
|
||||
SkipCertVerify bool `proxy:"skip-cert-verify,omitempty"`
|
||||
Network string `proxy:"network,omitempty"`
|
||||
|
||||
CongestionController string `proxy:"congestion-controller,omitempty"`
|
||||
CWND int `proxy:"cwnd,omitempty"`
|
||||
@@ -150,12 +154,26 @@ func NewMasque(option MasqueOption) (*Masque, error) {
|
||||
sni = masque.ConnectSNI
|
||||
}
|
||||
|
||||
tlsConfig, err := masque.PrepareTlsConfig(privKey, ecPubKey, sni)
|
||||
tlsConfig, err := masque.PrepareTlsConfig(privKey, ecPubKey, sni, option.SkipCertVerify)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to prepare TLS config: %v\n", err)
|
||||
}
|
||||
outbound.tlsConfig = tlsConfig
|
||||
|
||||
if option.Network == "h2" {
|
||||
tlsConfig.NextProtos = []string{"h2"}
|
||||
outbound.h2Transport = &http.Http2Transport{
|
||||
DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
c, err := outbound.dialer.DialContext(ctx, "tcp", outbound.addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tls.Client(c, tlsConfig), nil
|
||||
},
|
||||
ReadIdleTimeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
outbound.quicConfig = &quic.Config{
|
||||
EnableDatagrams: true,
|
||||
InitialPacketSize: 1242,
|
||||
@@ -229,27 +247,43 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
w.runDevice.Store(true)
|
||||
}
|
||||
|
||||
udpAddr, err := resolveUDPAddr(ctx, "udp", w.addr, w.prefer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pc net.PacketConn
|
||||
var closer io.Closer
|
||||
var ipConn masque.IpConn
|
||||
var err error
|
||||
if w.h2Transport != nil {
|
||||
closer, ipConn, err = masque.ConnectTunnelH2(ctx, w.h2Transport, w.uri)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
var udpAddr *net.UDPAddr
|
||||
udpAddr, err = resolveUDPAddr(ctx, "udp", w.addr, w.prefer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pc, err := w.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pc, err = w.dialer.ListenPacket(ctx, "udp", "", udpAddr.AddrPort())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
quicConn, err := quic.Dial(ctx, pc, udpAddr, w.tlsConfig, w.quicConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
transport := quic.Transport{Conn: pc}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.Dial(ctx, udpAddr, w.tlsConfig, w.quicConfig)
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
|
||||
common.SetCongestionController(quicConn, w.option.CongestionController, w.option.CWND)
|
||||
|
||||
tr, ipConn, err := masque.ConnectTunnel(ctx, quicConn, w.uri)
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
return err
|
||||
closer, ipConn, err = masque.ConnectTunnel(ctx, quicConn, w.uri)
|
||||
if err != nil {
|
||||
_ = pc.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w.running.Store(true)
|
||||
@@ -258,8 +292,10 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
contextutils.AfterFunc(runCtx, func() {
|
||||
w.running.Store(false)
|
||||
_ = ipConn.Close()
|
||||
_ = tr.Close()
|
||||
_ = pc.Close()
|
||||
_ = closer.Close()
|
||||
if pc != nil {
|
||||
_ = pc.Close()
|
||||
}
|
||||
})
|
||||
|
||||
go func() {
|
||||
@@ -276,7 +312,7 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
}
|
||||
icmp, err := ipConn.WritePacket(buf[:sizes[0]])
|
||||
if err != nil {
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err)
|
||||
return
|
||||
}
|
||||
@@ -299,7 +335,7 @@ func (w *Masque) run(ctx context.Context) error {
|
||||
for runCtx.Err() == nil {
|
||||
n, err := ipConn.ReadPacket(buf)
|
||||
if err != nil {
|
||||
if errors.As(err, new(*connectip.CloseError)) {
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
log.Errorln("[Masque](%s) connection closed while writing to IP connection: %v", w.name, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -600,7 +600,10 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, packetConn, udpAddr, tlsConfig, cfg)
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.DialEarly(ctx, udpAddr, tlsConfig, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
@@ -752,7 +755,10 @@ func NewVless(option VlessOption) (*Vless, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, packetConn, udpAddr, tlsConfig, cfg)
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.DialEarly(ctx, udpAddr, tlsConfig, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
|
||||
+8
-3
@@ -554,11 +554,11 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.
|
||||
IP: net.ParseIP(ip),
|
||||
Port: portInt,
|
||||
}
|
||||
conn, err := doh.dialer.ListenPacket(ctx, "udp", addr)
|
||||
packetConn, err := doh.dialer.ListenPacket(ctx, "udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transport := quic.Transport{Conn: conn}
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
tlsCfg = tlsCfg.Clone()
|
||||
@@ -568,7 +568,12 @@ func (doh *dnsOverHTTPS) dialQuic(ctx context.Context, addr string, tlsCfg *tls.
|
||||
// It's ok if net.SplitHostPort returns an error - it could be a hostname/IP address without a port.
|
||||
tlsCfg.ServerName = doh.url.Host
|
||||
}
|
||||
return transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg)
|
||||
quicConn, err := transport.DialEarly(ctx, &udpAddr, tlsCfg, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
return quicConn, nil
|
||||
}
|
||||
|
||||
// probeH3 runs a test to check whether QUIC is faster than TLS for this
|
||||
|
||||
+6
-5
@@ -279,7 +279,7 @@ func (doq *dnsOverQUIC) openStream(ctx context.Context, conn *quic.Conn) (*quic.
|
||||
}
|
||||
|
||||
// openConnection opens a new QUIC connection.
|
||||
func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, err error) {
|
||||
func (doq *dnsOverQUIC) openConnection(ctx context.Context) (quicConn *quic.Conn, err error) {
|
||||
// we're using bootstrapped address instead of what's passed to the function
|
||||
// it does not create an actual connection, but it helps us determine
|
||||
// what IP is actually reachable (when there're v4/v6 addresses).
|
||||
@@ -298,7 +298,7 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, er
|
||||
|
||||
p, err := strconv.Atoi(port)
|
||||
udpAddr := net.UDPAddr{IP: net.ParseIP(ip), Port: p}
|
||||
udp, err := doq.dialer.ListenPacket(ctx, "udp", addr)
|
||||
packetConn, err := doq.dialer.ListenPacket(ctx, "udp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -322,15 +322,16 @@ func (doq *dnsOverQUIC) openConnection(ctx context.Context) (conn *quic.Conn, er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
transport := quic.Transport{Conn: udp}
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
conn, err = transport.Dial(ctx, &udpAddr, tlsConfig, doq.getQUICConfig())
|
||||
quicConn, err = transport.Dial(ctx, &udpAddr, tlsConfig, doq.getQUICConfig())
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, fmt.Errorf("opening quic connection to %s: %w", doq.addr, err)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
return quicConn, nil
|
||||
}
|
||||
|
||||
// closeConnWithError closes the active connection with error to make sure that
|
||||
|
||||
@@ -1080,6 +1080,23 @@ proxies: # socks5
|
||||
# dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效
|
||||
# congestion-controller: bbr # 默认不开启
|
||||
|
||||
# masque-h2
|
||||
- name: "masque-h2"
|
||||
type: masque
|
||||
server: 162.159.198.2
|
||||
port: 443
|
||||
private-key: MHcCAQEEILI1eOtnbEIh89Fj4yNDuFR6UjayCKI3NdLl3DhetimWoAoGCCqGSM49AwEHoUQDQgAEgyXrE8v+hHsHy3ewSb3WcRjYgCrM9T9hiE0Uv6k2DZ1+4kefrDT9v1Q/8wdRigTf6t6gGNUV8W+IUMdrfUt+9g==
|
||||
public-key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIaU7MToJm9NKp8YfGxR6r+/h4mcG7SxI8tsW8OR1A5tv/zCzVbCRRh2t87/kxnP6lAy0lkr7qYwu+ox+k3dr6w==
|
||||
ip: 172.16.0.2
|
||||
ipv6: 2606:4700:110:84c0:163a:4914:a0ad:3342
|
||||
mtu: 1280
|
||||
udp: true
|
||||
network: h2
|
||||
# 一个出站代理的标识。当值不为空时,将使用指定的 proxy 发出连接
|
||||
# dialer-proxy: "ss1"
|
||||
# remote-dns-resolve: true # 强制 dns 远程解析,默认值为 false
|
||||
# dns: [ 1.1.1.1, 8.8.8.8 ] # 仅在 remote-dns-resolve 为 true 时生效
|
||||
|
||||
# tuic
|
||||
- name: tuic
|
||||
server: www.example.com
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ require (
|
||||
github.com/bahlo/generic-list-go v0.2.0
|
||||
github.com/coreos/go-iptables v0.8.0
|
||||
github.com/dlclark/regexp2 v1.11.5
|
||||
github.com/enfein/mieru/v3 v3.30.0
|
||||
github.com/enfein/mieru/v3 v3.30.1
|
||||
github.com/gobwas/ws v1.4.0
|
||||
github.com/gofrs/uuid/v5 v5.4.0
|
||||
github.com/golang/snappy v1.0.0
|
||||
|
||||
+2
-2
@@ -20,8 +20,8 @@ github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZ
|
||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/dunglas/httpsfv v1.0.2 h1:iERDp/YAfnojSDJ7PW3dj1AReJz4MrwbECSSE59JWL0=
|
||||
github.com/dunglas/httpsfv v1.0.2/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg=
|
||||
github.com/enfein/mieru/v3 v3.30.0 h1:g7v0TuK7y0ZMn6TOdjOs8WEUQk8bvs6WYPBJ16SKdBU=
|
||||
github.com/enfein/mieru/v3 v3.30.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/enfein/mieru/v3 v3.30.1 h1:gHHXQfpQO/5d789o9kokVfej7jl795aJwPihUk3gTDU=
|
||||
github.com/enfein/mieru/v3 v3.30.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358 h1:kXYqH/sL8dS/FdoFjr12ePjnLPorPo2FsnrHNuXSDyo=
|
||||
github.com/ericlagergren/aegis v0.0.0-20250325060835-cd0defd64358/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
|
||||
github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
package masque
|
||||
|
||||
// copy and modify from: https://github.com/Diniboy1123/connect-ip-go/blob/8d7bb0a858a2674046a7cb5538749e4c826c3538/client_h2.go
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/metacubex/mihomo/common/contextutils"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
|
||||
"github.com/metacubex/http"
|
||||
"github.com/metacubex/quic-go/quicvarint"
|
||||
"github.com/yosida95/uritemplate/v3"
|
||||
)
|
||||
|
||||
const h2DatagramCapsuleType uint64 = 0
|
||||
|
||||
const (
|
||||
ipv4HeaderLen = 20
|
||||
ipv6HeaderLen = 40
|
||||
)
|
||||
|
||||
func ConnectTunnelH2(ctx context.Context, h2Transport *http.Http2Transport, connectUri string) (*http.Http2ClientConn, IpConn, error) {
|
||||
additionalHeaders := http.Header{
|
||||
"User-Agent": []string{""},
|
||||
}
|
||||
template := uritemplate.MustNew(connectUri)
|
||||
|
||||
h2Headers := additionalHeaders.Clone()
|
||||
h2Headers.Set("cf-connect-proto", "cf-connect-ip")
|
||||
// TODO: support PQC
|
||||
h2Headers.Set("pq-enabled", "false")
|
||||
|
||||
conn, err := h2Transport.DialTLSContext(ctx, "tcp", ":0", nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to dial: %w", err)
|
||||
}
|
||||
|
||||
cc, err := h2Transport.NewClientConn(conn)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to create client connection: %w", err)
|
||||
}
|
||||
|
||||
if !cc.ReserveNewRequest() {
|
||||
_ = cc.Close()
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to reserve client connection: %w", err)
|
||||
}
|
||||
|
||||
ipConn, rsp, err := dialH2(ctx, cc, template, h2Headers)
|
||||
if err != nil {
|
||||
_ = cc.Close()
|
||||
if strings.Contains(err.Error(), "tls: access denied") {
|
||||
return nil, nil, errors.New("login failed! Please double-check if your tls key and cert is enrolled in the Cloudflare Access service")
|
||||
}
|
||||
return nil, nil, fmt.Errorf("failed to dial connect-ip over HTTP/2: %w", err)
|
||||
}
|
||||
|
||||
if rsp.StatusCode != http.StatusOK {
|
||||
_ = ipConn.Close()
|
||||
_ = cc.Close()
|
||||
return nil, nil, fmt.Errorf("failed to dial connect-ip: %v", rsp.Status)
|
||||
}
|
||||
|
||||
return cc, ipConn, nil
|
||||
}
|
||||
|
||||
// dialH2 dials a proxied connection over HTTP/2 CONNECT-IP.
|
||||
//
|
||||
// This transport carries proxied packets inside HTTP capsule DATAGRAM frames.
|
||||
func dialH2(ctx context.Context, rt http.RoundTripper, template *uritemplate.Template, additionalHeaders http.Header) (*h2IpConn, *http.Response, error) {
|
||||
if len(template.Varnames()) > 0 {
|
||||
return nil, nil, errors.New("connect-ip: IP flow forwarding not supported")
|
||||
}
|
||||
|
||||
u, err := url.Parse(template.Raw())
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to parse URI: %w", err)
|
||||
}
|
||||
|
||||
reqCtx, cancel := context.WithCancel(context.Background()) // reqCtx must disconnect from ctx, otherwise ctx would close the entire HTTP/2 connection.
|
||||
|
||||
pr, pw := io.Pipe()
|
||||
req, err := http.NewRequestWithContext(reqCtx, http.MethodConnect, u.String(), pr)
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to create request: %w", err)
|
||||
}
|
||||
req.Host = authorityFromURL(u)
|
||||
req.ContentLength = -1
|
||||
req.Header = make(http.Header)
|
||||
for k, v := range additionalHeaders {
|
||||
req.Header[k] = v
|
||||
}
|
||||
|
||||
stop := contextutils.AfterFunc(ctx, cancel) // temporarily connect ctx with reqCtx when client.Do
|
||||
rsp, err := rt.RoundTrip(req)
|
||||
stop() // disconnect ctx with reqCtx after client.Do
|
||||
if err != nil {
|
||||
cancel()
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
return nil, nil, fmt.Errorf("connect-ip: failed to send request: %w", err)
|
||||
}
|
||||
if rsp.StatusCode < 200 || rsp.StatusCode > 299 {
|
||||
cancel()
|
||||
_ = pr.Close()
|
||||
_ = pw.Close()
|
||||
_ = rsp.Body.Close()
|
||||
return nil, rsp, fmt.Errorf("connect-ip: server responded with %d", rsp.StatusCode)
|
||||
}
|
||||
|
||||
stream := &h2DatagramStream{
|
||||
requestBody: pw,
|
||||
responseBody: rsp.Body,
|
||||
cancel: cancel,
|
||||
}
|
||||
return &h2IpConn{
|
||||
str: stream,
|
||||
closeChan: make(chan struct{}),
|
||||
}, rsp, nil
|
||||
}
|
||||
|
||||
func authorityFromURL(u *url.URL) string {
|
||||
if u.Port() != "" {
|
||||
return u.Host
|
||||
}
|
||||
host := u.Hostname()
|
||||
if host == "" {
|
||||
return u.Host
|
||||
}
|
||||
return host + ":443"
|
||||
}
|
||||
|
||||
type h2IpConn struct {
|
||||
str *h2DatagramStream
|
||||
|
||||
mu sync.Mutex
|
||||
|
||||
closeChan chan struct{}
|
||||
closeErr error
|
||||
}
|
||||
|
||||
func (c *h2IpConn) ReadPacket(b []byte) (n int, err error) {
|
||||
start:
|
||||
data, err := c.str.ReceiveDatagram(context.Background())
|
||||
if err != nil {
|
||||
defer func() {
|
||||
// There are no errors that can be recovered in h2 mode,
|
||||
// so calling Close allows the outer read loop to exit in the next iteration by returning net.ErrClosed.
|
||||
_ = c.Close()
|
||||
}()
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return 0, c.closeErr
|
||||
default:
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err := c.handleIncomingProxiedPacket(data); err != nil {
|
||||
log.Debugln("dropping proxied packet: %s", err)
|
||||
goto start
|
||||
}
|
||||
return copy(b, data), nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) handleIncomingProxiedPacket(data []byte) error {
|
||||
if len(data) == 0 {
|
||||
return errors.New("connect-ip: empty packet")
|
||||
}
|
||||
switch v := ipVersion(data); v {
|
||||
default:
|
||||
return fmt.Errorf("connect-ip: unknown IP versions: %d", v)
|
||||
case 4:
|
||||
if len(data) < ipv4HeaderLen {
|
||||
return fmt.Errorf("connect-ip: malformed datagram: too short")
|
||||
}
|
||||
case 6:
|
||||
if len(data) < ipv6HeaderLen {
|
||||
return fmt.Errorf("connect-ip: malformed datagram: too short")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WritePacket writes an IP packet to the stream.
|
||||
// If sending the packet fails, it might return an ICMP packet.
|
||||
// It is the caller's responsibility to send the ICMP packet to the sender.
|
||||
func (c *h2IpConn) WritePacket(b []byte) (icmp []byte, err error) {
|
||||
data, err := c.composeDatagram(b)
|
||||
if err != nil {
|
||||
log.Debugln("dropping proxied packet (%d bytes) that can't be proxied: %s", len(b), err)
|
||||
return nil, nil
|
||||
}
|
||||
if err := c.str.SendDatagram(data); err != nil {
|
||||
select {
|
||||
case <-c.closeChan:
|
||||
return nil, c.closeErr
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) composeDatagram(b []byte) ([]byte, error) {
|
||||
// TODO: implement src, dst and ipproto checks
|
||||
if len(b) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
switch v := ipVersion(b); v {
|
||||
default:
|
||||
return nil, fmt.Errorf("connect-ip: unknown IP versions: %d", v)
|
||||
case 4:
|
||||
if len(b) < ipv4HeaderLen {
|
||||
return nil, fmt.Errorf("connect-ip: IPv4 packet too short")
|
||||
}
|
||||
ttl := b[8]
|
||||
if ttl <= 1 {
|
||||
return nil, fmt.Errorf("connect-ip: datagram TTL too small: %d", ttl)
|
||||
}
|
||||
b[8]-- // decrement TTL
|
||||
// recalculate the checksum
|
||||
binary.BigEndian.PutUint16(b[10:12], calculateIPv4Checksum(([ipv4HeaderLen]byte)(b[:ipv4HeaderLen])))
|
||||
case 6:
|
||||
if len(b) < ipv6HeaderLen {
|
||||
return nil, fmt.Errorf("connect-ip: IPv6 packet too short")
|
||||
}
|
||||
hopLimit := b[7]
|
||||
if hopLimit <= 1 {
|
||||
return nil, fmt.Errorf("connect-ip: datagram Hop Limit too small: %d", hopLimit)
|
||||
}
|
||||
b[7]-- // Decrement Hop Limit
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (c *h2IpConn) Close() error {
|
||||
c.mu.Lock()
|
||||
if c.closeErr == nil {
|
||||
c.closeErr = net.ErrClosed
|
||||
close(c.closeChan)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
err := c.str.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
func ipVersion(b []byte) uint8 { return b[0] >> 4 }
|
||||
|
||||
func calculateIPv4Checksum(header [ipv4HeaderLen]byte) uint16 {
|
||||
// add every 16-bit word in the header, skipping the checksum field (bytes 10 and 11)
|
||||
var sum uint32
|
||||
for i := 0; i < len(header); i += 2 {
|
||||
if i == 10 {
|
||||
continue // skip checksum field
|
||||
}
|
||||
sum += uint32(binary.BigEndian.Uint16(header[i : i+2]))
|
||||
}
|
||||
for (sum >> 16) > 0 {
|
||||
sum = (sum & 0xffff) + (sum >> 16)
|
||||
}
|
||||
return ^uint16(sum)
|
||||
}
|
||||
|
||||
type h2DatagramStream struct {
|
||||
requestBody *io.PipeWriter
|
||||
responseBody io.ReadCloser
|
||||
cancel context.CancelFunc
|
||||
|
||||
readMu sync.Mutex
|
||||
writeMu sync.Mutex
|
||||
}
|
||||
|
||||
func (s *h2DatagramStream) ReceiveDatagram(_ context.Context) ([]byte, error) {
|
||||
s.readMu.Lock()
|
||||
defer s.readMu.Unlock()
|
||||
|
||||
reader := quicvarint.NewReader(s.responseBody)
|
||||
for {
|
||||
capsuleType, err := quicvarint.Read(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payloadLen, err := quicvarint.Read(reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload := make([]byte, payloadLen)
|
||||
_, err = io.ReadFull(reader, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if capsuleType != h2DatagramCapsuleType {
|
||||
continue
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *h2DatagramStream) SendDatagram(data []byte) error {
|
||||
frame := make([]byte, 0, quicvarint.Len(h2DatagramCapsuleType)+quicvarint.Len(uint64(len(data)))+len(data))
|
||||
frame = quicvarint.Append(frame, h2DatagramCapsuleType)
|
||||
frame = quicvarint.Append(frame, uint64(len(data)))
|
||||
frame = append(frame, data...)
|
||||
|
||||
s.writeMu.Lock()
|
||||
defer s.writeMu.Unlock()
|
||||
_, err := s.requestBody.Write(frame)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect-ip: failed to send datagram capsule: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *h2DatagramStream) Close() error {
|
||||
_ = s.requestBody.Close()
|
||||
err := s.responseBody.Close()
|
||||
s.cancel()
|
||||
return err
|
||||
}
|
||||
@@ -27,9 +27,15 @@ const (
|
||||
ConnectURI = "https://cloudflareaccess.com"
|
||||
)
|
||||
|
||||
type IpConn interface {
|
||||
ReadPacket(b []byte) (n int, err error)
|
||||
WritePacket(b []byte) (icmp []byte, err error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// PrepareTlsConfig creates a TLS configuration using the provided certificate and SNI (Server Name Indication).
|
||||
// It also verifies the peer's public key against the provided public key.
|
||||
func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sni string) (*tls.Config, error) {
|
||||
func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sni string, insecure bool) (*tls.Config, error) {
|
||||
verfiyCert := func(cert *x509.Certificate) error {
|
||||
if _, ok := cert.PublicKey.(*ecdsa.PublicKey); !ok {
|
||||
// we only support ECDSA
|
||||
@@ -77,6 +83,9 @@ func PrepareTlsConfig(privKey *ecdsa.PrivateKey, peerPubKey *ecdsa.PublicKey, sn
|
||||
return err
|
||||
},
|
||||
}
|
||||
if insecure {
|
||||
tlsConfig.VerifyConnection = nil
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@ func (c *Client) quicRoundTripper(tlsConfig *vmess.TLSConfig, congestionControlN
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
quicConn, err := quic.DialEarly(ctx, packetConn, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
|
||||
transport := quic.Transport{Conn: packetConn}
|
||||
transport.SetCreatedConn(true) // auto close conn
|
||||
transport.SetSingleUse(true) // auto close transport
|
||||
quicConn, err := transport.DialEarly(ctx, net.UDPAddrFromAddrPort(addrPort), tlsCfg, cfg)
|
||||
if err != nil {
|
||||
_ = packetConn.Close()
|
||||
return nil, err
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openclash
|
||||
PKG_VERSION:=0.47.086
|
||||
PKG_VERSION:=0.47.088
|
||||
PKG_MAINTAINER:=vernesong <https://github.com/vernesong/OpenClash>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
@@ -265,17 +265,17 @@ o.placeholder = translate("5000 or 1234-2345")
|
||||
o.rmempty = true
|
||||
|
||||
o = s2:option(ListValue, "proto", translate("Proto"))
|
||||
o:value("both", translate("Both"))
|
||||
o:value("udp", translate("UDP"))
|
||||
o:value("tcp", translate("TCP"))
|
||||
o:value("both", translate("Both"))
|
||||
o.default = "tcp"
|
||||
o.default = "both"
|
||||
o.rmempty = false
|
||||
|
||||
o = s2:option(ListValue, "family", translate("Family"))
|
||||
o:value("both", translate("Both"))
|
||||
o:value("ipv4", translate("IPv4"))
|
||||
o:value("ipv6", translate("IPv6"))
|
||||
o:value("both", translate("Both"))
|
||||
o.default = "tcp"
|
||||
o.default = "both"
|
||||
o.rmempty = false
|
||||
|
||||
o = s2:option(ListValue, "interface", translate("Interface"))
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 10000;
|
||||
z-index: 5;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -296,6 +296,13 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.oc .sub-card:hover {
|
||||
background: var(--hover-bg, #f3f4f6);
|
||||
border-color: var(--primary-color, #3b82f6);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.oc .sub-card.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -870,13 +870,21 @@ upnp_exclude()
|
||||
ipv6_suffix_to_nft_format()
|
||||
{
|
||||
local ipv6_with_prefix="$1"
|
||||
if [[ "$ipv6_with_prefix" =~ / ]] || [ -n "$(echo ${ipv6_with_prefix} | grep '/')" ]; then
|
||||
local suffix="${ipv6_with_prefix%%/*}"
|
||||
local prefix="${ipv6_with_prefix##*/}"
|
||||
echo "& $prefix == $suffix"
|
||||
else
|
||||
echo "$ipv6_with_prefix"
|
||||
|
||||
if ! echo "$ipv6_with_prefix" | grep -q '/'; then
|
||||
echo "{ $ipv6_with_prefix }"
|
||||
return
|
||||
fi
|
||||
|
||||
local addr="${ipv6_with_prefix%%/*}"
|
||||
local suffix="${ipv6_with_prefix##*/}"
|
||||
|
||||
if echo "$suffix" | grep -qE '^[0-9]+$'; then
|
||||
echo "${addr}/${suffix}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "& ${suffix} == ${addr}"
|
||||
} 2>/dev/null
|
||||
|
||||
firewall_lan_ac_traffic()
|
||||
@@ -3718,7 +3726,12 @@ get_config()
|
||||
else
|
||||
fakeip_range6=$(uci_get_config "fakeip_range6")
|
||||
fi
|
||||
[ -z "$fakeip_range6" ] && fakeip_range6=0
|
||||
if [ -z "$fakeip_range6" ]; then
|
||||
fakeip_range6="fdfe:dcba:9876::1/64"
|
||||
fake_ip_range6_enable=0
|
||||
else
|
||||
fake_ip_range6_enable=1
|
||||
fi
|
||||
|
||||
lan_interface_name=$(uci_get_config "lan_interface_name" || echo 0)
|
||||
if [ "$lan_interface_name" = "0" ]; then
|
||||
@@ -3790,7 +3803,7 @@ start_service()
|
||||
enable=$(uci_get_config "enable")
|
||||
[ "$enable" != "1" ] && LOG_WARN "OpenClash Now Disabled, Need Start From Luci Page, Exit..." && SLOG_CLEAN && exit 0
|
||||
|
||||
if /etc/init.d/openclash status >/dev/null 2>&1; then
|
||||
if procd_running "openclash" >/dev/null; then
|
||||
LOG_TIP "OpenClash Already Running, Exit..."
|
||||
exit 0
|
||||
fi
|
||||
@@ -3820,7 +3833,7 @@ start_service()
|
||||
"$find_process_mode" "$fakeip_range" "$ipv6_mode" "$stack_type_v6" "$enable_unified_delay"\
|
||||
"$enable_respect_rules" "$custom_fakeip_filter_mode" "$iptables_compat" "$disable_quic_go_gso" "$cors_allow"\
|
||||
"$geo_custom_url" "$geoip_custom_url" "$geosite_custom_url" "$geoasn_custom_url"\
|
||||
"$lgbm_auto_update" "$lgbm_custom_url" "$lgbm_update_interval" "$smart_collect" "$smart_collect_size" "$fakeip_range6"
|
||||
"$lgbm_auto_update" "$lgbm_custom_url" "$lgbm_update_interval" "$smart_collect" "$smart_collect_size" "$fakeip_range6" "$fake_ip_range6_enable"
|
||||
|
||||
/usr/share/openclash/yml_rules_change.sh \
|
||||
"$enable_custom_clash_rules" "$TMP_CONFIG_FILE"\
|
||||
@@ -3905,6 +3918,12 @@ stop_service()
|
||||
done
|
||||
# prevent respawn during stopping
|
||||
procd_kill "openclash"
|
||||
for i in $(seq 1 10); do
|
||||
procd_running "openclash" >/dev/null && sleep 1 || break
|
||||
done
|
||||
if procd_running "openclash" >/dev/null; then
|
||||
kill -9 $(pidof clash) 2>/dev/null || true
|
||||
fi
|
||||
|
||||
LOG_OUT "Step 4: Restart Dnsmasq..."
|
||||
revert_dnsmasq
|
||||
|
||||
@@ -86,17 +86,6 @@ DOWNLOAD_RESULT=$?
|
||||
|
||||
config_cus_up()
|
||||
{
|
||||
if [ -z "$CONFIG_PATH" ]; then
|
||||
for file_name in /etc/openclash/config/*
|
||||
do
|
||||
if [ -f "$file_name" ]; then
|
||||
CONFIG_PATH=$file_name
|
||||
break
|
||||
fi
|
||||
done
|
||||
uci -q set openclash.config.config_path="$CONFIG_PATH"
|
||||
uci commit openclash
|
||||
fi
|
||||
if [ -z "$subscribe_url_param" ]; then
|
||||
if [ -n "$key_match_param" ] || [ -n "$key_ex_match_param" ]; then
|
||||
LOG_OUT "Config File【$name】Start Picking Nodes..."
|
||||
@@ -192,6 +181,10 @@ config_su_check()
|
||||
mv "$CFG_FILE" "$CONFIG_FILE" 2>/dev/null
|
||||
LOG_OUT "Config File【$name】Update Successful!"
|
||||
fi
|
||||
if [ -z "$CONFIG_PATH" ]; then
|
||||
uci -q set openclash.config.config_path="$CONFIG_FILE"
|
||||
uci commit openclash
|
||||
fi
|
||||
if [ "$CONFIG_FILE" == "$CONFIG_PATH" ]; then
|
||||
restart=1
|
||||
fi
|
||||
|
||||
@@ -378,6 +378,7 @@ begin
|
||||
smart_collect = '${44}' == '1'
|
||||
smart_collect_size = '${45}'
|
||||
fake_ip_range6 = '${46}'
|
||||
fake_ip_range6_enable = '${47}' == '1'
|
||||
default_dashboard = '$default_dashboard'
|
||||
yacd_type = '$yacd_type'
|
||||
dashboard_type = '$dashboard_type'
|
||||
@@ -480,7 +481,7 @@ begin
|
||||
else
|
||||
Value['dns']['enhanced-mode'] = 'fake-ip'
|
||||
Value['dns']['fake-ip-range'] = fake_ip_range
|
||||
if Value['dns']['ipv6'] and fake_ip_range6 != '0'
|
||||
if Value['dns']['ipv6'] and fake_ip_range6_enable
|
||||
Value['dns']['fake-ip-range6'] = fake_ip_range6
|
||||
end
|
||||
end
|
||||
|
||||
+2
-1
@@ -135,7 +135,8 @@ end
|
||||
|
||||
o = s:option(ListValue, "domain_resolver", translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" ..
|
||||
translate("Supports only Xray or Sing-box node types.")
|
||||
translate("Supports only Xray or Sing-box node types.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
|
||||
@@ -725,7 +725,9 @@ o.datatype = "uinteger"
|
||||
o.placeholder = 0
|
||||
o:depends({ [_n("protocol")] = "vless" })
|
||||
|
||||
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution."))
|
||||
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
|
||||
+3
-1
@@ -732,7 +732,9 @@ o:value("v2ray-plugin")
|
||||
o = s:option(Value, _n("plugin_opts"), translate("opts"))
|
||||
o:depends({ [_n("plugin_enabled")] = true })
|
||||
|
||||
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"), translate("If the node address is a domain name, this DNS will be used for resolution."))
|
||||
o = s:option(ListValue, _n("domain_resolver"), translate("Domain DNS Resolve"))
|
||||
o.description = translate("If the node address is a domain name, this DNS will be used for resolution.") .. "<br>" .. string.format('<font color="red">%s</font>',
|
||||
translate("Note: For node-specific DNS only. Keep Auto to avoid extra overhead."))
|
||||
o:value("", translate("Auto"))
|
||||
o:value("tcp", "TCP")
|
||||
o:value("udp", "UDP")
|
||||
|
||||
@@ -12,7 +12,8 @@ local local_version = api.get_app_version("sing-box"):match("[^v]+")
|
||||
local version_ge_1_13_0 = api.compare_versions(local_version, ">=", "1.13.0")
|
||||
|
||||
local GLOBAL = {
|
||||
DNS_SERVER = {}
|
||||
DNS_SERVER = {},
|
||||
VPS_EXCLUDE = {}
|
||||
}
|
||||
|
||||
local GEO_VAR = {
|
||||
@@ -159,7 +160,6 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
}
|
||||
|
||||
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
|
||||
local dns_tag = node_id .. "_dns"
|
||||
local dns_proto = node.domain_resolver
|
||||
local server_address
|
||||
local server_port
|
||||
@@ -168,12 +168,8 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
local _a = api.parseURL(node.domain_resolver_dns_https)
|
||||
if _a then
|
||||
server_address = _a.hostname
|
||||
if _a.port then
|
||||
server_port = _a.port
|
||||
else
|
||||
server_port = 443
|
||||
end
|
||||
server_path = _a.pathname
|
||||
server_port = _a.port or 443
|
||||
server_path = _a.pathname or ""
|
||||
end
|
||||
else
|
||||
server_address = node.domain_resolver_dns
|
||||
@@ -184,19 +180,28 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
server_port = tonumber(split[#split])
|
||||
end
|
||||
end
|
||||
GLOBAL.DNS_SERVER[node_id] = {
|
||||
server = {
|
||||
tag = dns_tag,
|
||||
type = dns_proto,
|
||||
server = server_address,
|
||||
server_port = server_port,
|
||||
path = server_path,
|
||||
domain_resolver = "direct",
|
||||
detour = "direct"
|
||||
},
|
||||
domain = node.address
|
||||
}
|
||||
result.domain_resolver.server = dns_tag
|
||||
local dns_key = dns_proto .. "|" .. tostring(server_address) .. "|" .. tostring(server_port) .. "|" .. tostring(server_path or "")
|
||||
if not GLOBAL.DNS_SERVER[dns_key] then
|
||||
GLOBAL.DNS_SERVER[dns_key] = {
|
||||
server = {
|
||||
tag = "dns-node-" .. api.gen_short_uuid(),
|
||||
type = dns_proto,
|
||||
server = server_address,
|
||||
server_port = server_port,
|
||||
path = server_path,
|
||||
domain_resolver = "direct",
|
||||
detour = "direct"
|
||||
},
|
||||
domain = {}
|
||||
}
|
||||
end
|
||||
local exists
|
||||
for _, d in ipairs(GLOBAL.DNS_SERVER[dns_key].domain) do
|
||||
if d == node.address then exists = true; break end
|
||||
end
|
||||
if not exists then table.insert(GLOBAL.DNS_SERVER[dns_key].domain, node.address) end
|
||||
result.domain_resolver.server = GLOBAL.DNS_SERVER[dns_key].server.tag
|
||||
GLOBAL.VPS_EXCLUDE[node.address] = true
|
||||
end
|
||||
|
||||
local tls = nil
|
||||
@@ -1766,12 +1771,8 @@ function gen_config(var)
|
||||
remote_server.type = "h3"
|
||||
end
|
||||
remote_server.server = _a.hostname
|
||||
if _a.port then
|
||||
remote_server.server_port = _a.port
|
||||
else
|
||||
remote_server.server_port = 443
|
||||
end
|
||||
remote_server.path = _a.pathname
|
||||
remote_server.server_port = _a.port or 443
|
||||
remote_server.path = _a.pathname or ""
|
||||
end
|
||||
if remote_dns_doh_ip and remote_dns_doh_host ~= remote_dns_doh_ip and not api.is_ip(remote_dns_doh_host) then
|
||||
local domains = {}
|
||||
@@ -1825,9 +1826,11 @@ function gen_config(var)
|
||||
|
||||
if direct_dns_udp_server or direct_dns_tcp_server then
|
||||
local domain = {}
|
||||
local nodes_domain_text = sys.exec('uci show passwall | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
|
||||
local nodes_domain_text = sys.exec([[uci show passwall | sed -n "s/.*\.address='\([^']*\)'/\1/p" | sort -u]])
|
||||
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
|
||||
table.insert(domain, w)
|
||||
if w and w ~= "" and api.datatypes.hostname(w) and not GLOBAL.VPS_EXCLUDE[w] then
|
||||
table.insert(domain, w)
|
||||
end
|
||||
end)
|
||||
if #domain > 0 then
|
||||
table.insert(dns_domain_rules, 1, {
|
||||
@@ -1970,9 +1973,11 @@ function gen_config(var)
|
||||
for i, v in pairs(GLOBAL.DNS_SERVER) do
|
||||
table.insert(dns.servers, v.server)
|
||||
if not dns.rules then dns.rules = {} end
|
||||
table.insert(dns.rules, {
|
||||
table.insert(dns.rules, 1, {
|
||||
action = "route",
|
||||
server = v.server.tag,
|
||||
disable_cache = false,
|
||||
rewrite_ttl = 30,
|
||||
domain = v.domain,
|
||||
})
|
||||
end
|
||||
|
||||
@@ -8,7 +8,8 @@ local fs = api.fs
|
||||
|
||||
local GLOBAL = {
|
||||
DNS_SERVER = {},
|
||||
DNS_HOSTNAME = {}
|
||||
DNS_HOSTNAME = {},
|
||||
VPS_EXCLUDE = {}
|
||||
}
|
||||
|
||||
local xray_version = api.get_app_version("xray")
|
||||
@@ -405,7 +406,6 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
|
||||
if api.datatypes.hostname(node.address) and node.domain_resolver and (node.domain_resolver_dns or node.domain_resolver_dns_https) then
|
||||
local dns_tag = node_id .. "_dns"
|
||||
local dns_proto = node.domain_resolver
|
||||
local config_address
|
||||
local config_port
|
||||
@@ -413,20 +413,14 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
local _a = api.parseURL(node.domain_resolver_dns_https)
|
||||
if _a then
|
||||
config_address = node.domain_resolver_dns_https
|
||||
if _a.port then
|
||||
config_port = _a.port
|
||||
else
|
||||
config_port = 443
|
||||
end
|
||||
if _a.hostname then
|
||||
if api.datatypes.hostname(_a.hostname) then
|
||||
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
|
||||
end
|
||||
config_port = _a.port or 443
|
||||
if _a.hostname and api.datatypes.hostname(_a.hostname) then
|
||||
GLOBAL.DNS_HOSTNAME[_a.hostname] = true
|
||||
end
|
||||
end
|
||||
else
|
||||
local server_address = node.domain_resolver_dns
|
||||
local config_port = 53
|
||||
config_port = 53
|
||||
local split = api.split(server_address, ":")
|
||||
if #split > 1 then
|
||||
server_address = split[1]
|
||||
@@ -437,13 +431,27 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
config_address = dns_proto .. "://" .. server_address .. ":" .. config_port
|
||||
end
|
||||
end
|
||||
GLOBAL.DNS_SERVER[node_id] = {
|
||||
tag = dns_tag,
|
||||
queryStrategy = node.domain_strategy or "UseIP",
|
||||
address = config_address,
|
||||
port = config_port,
|
||||
domains = {"full:" .. node.address}
|
||||
}
|
||||
local dns_key = dns_proto .. "|" .. config_address .. "|" .. tostring(config_port)
|
||||
if not GLOBAL.DNS_SERVER[dns_key] then
|
||||
GLOBAL.DNS_SERVER[dns_key] = {
|
||||
tag = "dns-node-" .. api.gen_short_uuid(),
|
||||
queryStrategy = node.domain_strategy or "UseIP",
|
||||
address = config_address,
|
||||
port = config_port,
|
||||
finalQuery = true,
|
||||
disableCache = false,
|
||||
serveStale = true,
|
||||
serveExpiredTTL = 30,
|
||||
domains = {}
|
||||
}
|
||||
end
|
||||
local exists
|
||||
local domain = "full:" .. node.address
|
||||
for _, d in ipairs(GLOBAL.DNS_SERVER[dns_key].domains) do
|
||||
if d == domain then exists = true; break end
|
||||
end
|
||||
if not exists then table.insert(GLOBAL.DNS_SERVER[dns_key].domains, domain) end
|
||||
GLOBAL.VPS_EXCLUDE[node.address] = true
|
||||
end
|
||||
end
|
||||
return result
|
||||
@@ -1509,7 +1517,8 @@ function gen_config(var)
|
||||
disableFallbackIfMatch = true,
|
||||
servers = {},
|
||||
clientIp = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
|
||||
queryStrategy = "UseIP"
|
||||
queryStrategy = "UseIP",
|
||||
useSystemHosts = true
|
||||
}
|
||||
|
||||
local _direct_dns = {
|
||||
@@ -1521,9 +1530,11 @@ function gen_config(var)
|
||||
|
||||
if direct_dns_udp_server or direct_dns_tcp_server then
|
||||
local domain = {}
|
||||
local nodes_domain_text = sys.exec('uci show passwall | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
|
||||
local nodes_domain_text = sys.exec([[uci show passwall | sed -n "s/.*\.\(address\|download_address\)='\([^']*\)'/\2/p" | sort -u]])
|
||||
string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
|
||||
table.insert(domain, w)
|
||||
if w and w ~= "" and api.datatypes.hostname(w) and not GLOBAL.VPS_EXCLUDE[w] then
|
||||
table.insert(domain, "full:" .. w)
|
||||
end
|
||||
end)
|
||||
if #domain > 0 then
|
||||
table.insert(dns_domain_rules, 1, {
|
||||
@@ -1763,9 +1774,10 @@ function gen_config(var)
|
||||
if #node_dns > 0 and #dns.servers < 1 then
|
||||
dns.servers = { "localhost" }
|
||||
end
|
||||
local idx = dns_listen_port and 2 or 1
|
||||
for i = #node_dns, 1, -1 do
|
||||
local value = node_dns[i]
|
||||
table.insert(routing.rules, 1, {
|
||||
table.insert(routing.rules, idx, {
|
||||
inboundTag = {
|
||||
value.server.tag
|
||||
},
|
||||
@@ -1784,7 +1796,7 @@ function gen_config(var)
|
||||
queryStrategy = "UseIPv4",
|
||||
domains = hostname
|
||||
})
|
||||
table.insert(routing.rules, 1, {
|
||||
table.insert(routing.rules, idx, {
|
||||
inboundTag = { "bootstrap" },
|
||||
outboundTag = "direct"
|
||||
})
|
||||
|
||||
@@ -1942,6 +1942,9 @@ msgstr "域名 DNS 解析"
|
||||
msgid "If the node address is a domain name, this DNS will be used for resolution."
|
||||
msgstr "如果节点地址是域名,则将使用此 DNS 进行解析。"
|
||||
|
||||
msgid "Note: For node-specific DNS only. Keep Auto to avoid extra overhead."
|
||||
msgstr "注意:仅用于节点专用 DNS,通常请保持自动,以免增加开销。"
|
||||
|
||||
msgid "Supports only Xray or Sing-box node types."
|
||||
msgstr "仅支持 Xray 或 Sing-box 类型节点。"
|
||||
|
||||
|
||||
@@ -186,7 +186,18 @@ run_singbox() {
|
||||
[ -n "$no_run" ] && json_add_string "no_run" "1"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_SINGBOX gen_config "${_json_arg}" > $config_file
|
||||
[ -n "$no_run" ] || ln_run "$SINGBOX_BIN" "sing-box" $log_file run -c "$config_file"
|
||||
[ -n "$no_run" ] && return
|
||||
|
||||
local test_log_file=$log_file
|
||||
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
|
||||
$SINGBOX_BIN check -c "$config_file" > $test_log_file 2>&1; local status=$?
|
||||
if [ "${status}" = 0 ]; then
|
||||
ln_run "$SINGBOX_BIN" "sing-box" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
echolog "Sing-box 配置文件 $config_file 校验有误,进程启动失败,错误信息:"
|
||||
cat ${test_log_file} >> ${LOG_FILE}
|
||||
fi
|
||||
[ "$test_log_file" != "$log_file" ] && rm -f "${test_log_file}"
|
||||
}
|
||||
|
||||
run_xray() {
|
||||
@@ -275,7 +286,18 @@ run_xray() {
|
||||
[ -n "$no_run" ] && json_add_string "no_run" "1"
|
||||
local _json_arg="$(json_dump)"
|
||||
lua $UTIL_XRAY gen_config "${_json_arg}" > $config_file
|
||||
[ -n "$no_run" ] || ln_run "$XRAY_BIN" "xray" $log_file run -c "$config_file"
|
||||
[ -n "$no_run" ] && return
|
||||
|
||||
local test_log_file=$log_file
|
||||
[ "$test_log_file" = "/dev/null" ] && test_log_file="${TMP_PATH}/${config_file##*/}_test.log"
|
||||
$XRAY_BIN run -test -c "$config_file" > $test_log_file; local status=$?
|
||||
if [ "${status}" = 0 ]; then
|
||||
ln_run "$XRAY_BIN" "xray" "${log_file}" run -c "$config_file"
|
||||
else
|
||||
echolog "Xray 配置文件 $config_file 校验有误,进程启动失败,错误信息:"
|
||||
cat ${test_log_file} >> ${LOG_FILE}
|
||||
fi
|
||||
[ "$test_log_file" != "$log_file" ] && rm -f "${test_log_file}"
|
||||
}
|
||||
|
||||
run_dns2socks() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
[<img src="https://img.shields.io/docker/pulls/esrrhs/pingtunnel">](https://hub.docker.com/repository/docker/esrrhs/pingtunnel)
|
||||
[<img src="https://img.shields.io/github/actions/workflow/status/esrrhs/pingtunnel/go.yml?branch=master">](https://github.com/esrrhs/pingtunnel/actions)
|
||||
|
||||
Pingtunnel is a tool that send TCP/UDP traffic over ICMP.
|
||||
Pingtunnel is a tool that sends TCP/UDP traffic over ICMP.
|
||||
|
||||
## Note: This tool is only to be used for study and research, do not use it for illegal purposes
|
||||
|
||||
@@ -38,11 +38,11 @@ echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
|
||||
|
||||
- Download the corresponding installation package from [releases](https://github.com/esrrhs/pingtunnel/releases), such as pingtunnel_windows64.zip, and decompress it
|
||||
- Then run with **administrator** privileges. The commands corresponding to different forwarding functions are as follows.
|
||||
- If you see a log of ping pong, the connection is normal
|
||||
- If you see ping/pong logs, the connection is normal
|
||||
- “-key” parameter is **int** type, only supports numbers between 0-2147483647
|
||||
|
||||
|
||||
#### Forward sock5
|
||||
#### Forward SOCKS5
|
||||
|
||||
```
|
||||
pingtunnel.exe -type client -l :4455 -s www.yourserver.com -sock5 1
|
||||
@@ -69,7 +69,7 @@ A dedicated Android client for pingtunnel is now available, developed by the com
|
||||
> Big thanks to [itismoej](https://github.com/itismoej) for developing this Android client!
|
||||
|
||||
### Use Docker
|
||||
It can also be started directly with docker, which is more convenient. Same parameters as above
|
||||
It can also be started directly with docker, which is more convenient. It uses the same parameters as above.
|
||||
- server:
|
||||
```
|
||||
docker run --name pingtunnel-server -d --privileged --network host --restart=always esrrhs/pingtunnel ./pingtunnel -type server -key 123456
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package adapter
|
||||
|
||||
import "context"
|
||||
|
||||
type TailscaleEndpoint interface {
|
||||
SubscribeTailscaleStatus(ctx context.Context, fn func(*TailscaleEndpointStatus)) error
|
||||
StartTailscalePing(ctx context.Context, peerIP string, fn func(*TailscalePingResult)) error
|
||||
}
|
||||
|
||||
type TailscalePingResult struct {
|
||||
LatencyMs float64
|
||||
IsDirect bool
|
||||
Endpoint string
|
||||
DERPRegionID int32
|
||||
DERPRegionCode string
|
||||
Error string
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatus struct {
|
||||
BackendState string
|
||||
AuthURL string
|
||||
NetworkName string
|
||||
MagicDNSSuffix string
|
||||
Self *TailscalePeer
|
||||
UserGroups []*TailscaleUserGroup
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
UserID int64
|
||||
LoginName string
|
||||
DisplayName string
|
||||
ProfilePicURL string
|
||||
Peers []*TailscalePeer
|
||||
}
|
||||
|
||||
type TailscalePeer struct {
|
||||
HostName string
|
||||
DNSName string
|
||||
OS string
|
||||
TailscaleIPs []string
|
||||
Online bool
|
||||
ExitNode bool
|
||||
ExitNodeOption bool
|
||||
Active bool
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
UserID int64
|
||||
KeyExpiry int64
|
||||
}
|
||||
+11
-4
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/sagernet/sing-box/common/tls"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
"github.com/sagernet/sing-box/dns/transport/local"
|
||||
"github.com/sagernet/sing-box/experimental"
|
||||
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
@@ -171,7 +170,10 @@ func New(options Options) (*Box, error) {
|
||||
|
||||
var internalServices []adapter.LifecycleService
|
||||
certificateOptions := common.PtrValueOrDefault(options.Certificate)
|
||||
if C.IsAndroid || C.IsDarwin || certificateOptions.Store != "" {
|
||||
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||
len(certificateOptions.Certificate) > 0 ||
|
||||
len(certificateOptions.CertificatePath) > 0 ||
|
||||
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -355,11 +357,12 @@ func New(options Options) (*Box, error) {
|
||||
)
|
||||
})
|
||||
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||
return local.NewTransport(
|
||||
return dnsTransportRegistry.CreateDNSTransport(
|
||||
ctx,
|
||||
logFactory.NewLogger("dns/local"),
|
||||
"local",
|
||||
option.LocalDNSServerOptions{},
|
||||
C.DNSTypeLocal,
|
||||
&option.LocalDNSServerOptions{},
|
||||
)
|
||||
})
|
||||
if platformInterface != nil {
|
||||
@@ -594,6 +597,10 @@ func (s *Box) Outbound() adapter.OutboundManager {
|
||||
return s.outbound
|
||||
}
|
||||
|
||||
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||
return s.endpoint
|
||||
}
|
||||
|
||||
func (s *Box) LogFactory() log.Factory {
|
||||
return s.logFactory
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@ import io.nekohasekai.sfa.compose.theme.SFATheme
|
||||
import io.nekohasekai.sfa.compose.topbar.LocalTopBarController
|
||||
import io.nekohasekai.sfa.compose.topbar.TopBarController
|
||||
import io.nekohasekai.sfa.compose.topbar.TopBarEntry
|
||||
import io.nekohasekai.sfa.constant.Action
|
||||
import io.nekohasekai.sfa.constant.Alert
|
||||
import io.nekohasekai.sfa.constant.ServiceMode
|
||||
import io.nekohasekai.sfa.constant.Status
|
||||
@@ -226,6 +227,10 @@ class MainActivity :
|
||||
pendingNavigationRoute.value = "settings/privilege"
|
||||
}
|
||||
val uri = intent.data ?: return
|
||||
if (intent.action == Action.OPEN_URL) {
|
||||
launchCustomTab(uri.toString())
|
||||
return
|
||||
}
|
||||
if (uri.scheme == "sing-box" && uri.host == "import-remote-profile") {
|
||||
try {
|
||||
val profile = Libbox.parseRemoteProfileImportLink(uri.toString())
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
VERSION_CODE=649
|
||||
VERSION_NAME=1.13.6
|
||||
VERSION_CODE=651
|
||||
VERSION_NAME=1.13.7
|
||||
GO_VERSION=go1.25.8
|
||||
|
||||
|
||||
|
||||
@@ -82,6 +82,11 @@ func compileRuleSet(sourcePath string) error {
|
||||
}
|
||||
|
||||
func downgradeRuleSetVersion(version uint8, options option.PlainRuleSet) uint8 {
|
||||
if version == C.RuleSetVersion5 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return len(rule.PackageNameRegex) > 0
|
||||
}) {
|
||||
version = C.RuleSetVersion4
|
||||
}
|
||||
if version == C.RuleSetVersion4 && !rule.HasHeadlessRule(options.Rules, func(rule option.DefaultHeadlessRule) bool {
|
||||
return rule.NetworkInterfaceAddress != nil && rule.NetworkInterfaceAddress.Size() > 0 ||
|
||||
len(rule.DefaultInterfaceAddress) > 0
|
||||
|
||||
@@ -16,6 +16,7 @@ var (
|
||||
commandNetworkQualityFlagConfigURL string
|
||||
commandNetworkQualityFlagSerial bool
|
||||
commandNetworkQualityFlagMaxRuntime int
|
||||
commandNetworkQualityFlagHTTP3 bool
|
||||
)
|
||||
|
||||
var commandNetworkQuality = &cobra.Command{
|
||||
@@ -45,6 +46,11 @@ func init() {
|
||||
"max-runtime", int(networkquality.DefaultMaxRuntime/time.Second),
|
||||
"Network quality maximum runtime in seconds",
|
||||
)
|
||||
commandNetworkQuality.Flags().BoolVar(
|
||||
&commandNetworkQualityFlagHTTP3,
|
||||
"http3", false,
|
||||
"Use HTTP/3 (QUIC) for measurement traffic",
|
||||
)
|
||||
commandTools.AddCommand(commandNetworkQuality)
|
||||
}
|
||||
|
||||
@@ -63,19 +69,25 @@ func runNetworkQuality() error {
|
||||
httpClient := networkquality.NewHTTPClient(dialer)
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(dialer, commandNetworkQualityFlagHTTP3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "==== NETWORK QUALITY TEST ====")
|
||||
|
||||
result, err := networkquality.Run(networkquality.Options{
|
||||
ConfigURL: commandNetworkQualityFlagConfigURL,
|
||||
HTTPClient: httpClient,
|
||||
Serial: commandNetworkQualityFlagSerial,
|
||||
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
|
||||
Context: globalCtx,
|
||||
ConfigURL: commandNetworkQualityFlagConfigURL,
|
||||
HTTPClient: httpClient,
|
||||
NewMeasurementClient: measurementClientFactory,
|
||||
Serial: commandNetworkQualityFlagSerial,
|
||||
MaxRuntime: time.Duration(commandNetworkQualityFlagMaxRuntime) * time.Second,
|
||||
Context: globalCtx,
|
||||
OnProgress: func(p networkquality.Progress) {
|
||||
if !commandNetworkQualityFlagSerial && p.Phase != networkquality.PhaseIdle {
|
||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d Upload: %s RPM: %d",
|
||||
formatBitrate(p.DownloadCapacity), p.DownloadRPM,
|
||||
formatBitrate(p.UploadCapacity), p.UploadRPM)
|
||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM,
|
||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
||||
return
|
||||
}
|
||||
switch networkquality.Phase(p.Phase) {
|
||||
@@ -87,10 +99,10 @@ func runNetworkQuality() error {
|
||||
}
|
||||
case networkquality.PhaseDownload:
|
||||
fmt.Fprintf(os.Stderr, "\rDownload: %s RPM: %d",
|
||||
formatBitrate(p.DownloadCapacity), p.DownloadRPM)
|
||||
networkquality.FormatBitrate(p.DownloadCapacity), p.DownloadRPM)
|
||||
case networkquality.PhaseUpload:
|
||||
fmt.Fprintf(os.Stderr, "\rUpload: %s RPM: %d",
|
||||
formatBitrate(p.UploadCapacity), p.UploadRPM)
|
||||
networkquality.FormatBitrate(p.UploadCapacity), p.UploadRPM)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -101,22 +113,9 @@ func runNetworkQuality() error {
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintln(os.Stderr, strings.Repeat("-", 40))
|
||||
fmt.Fprintf(os.Stderr, "Idle Latency: %d ms\n", result.IdleLatencyMs)
|
||||
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", formatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", formatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Download Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.DownloadCapacity), result.DownloadCapacityAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Upload Capacity: %-20s Accuracy: %s\n", networkquality.FormatBitrate(result.UploadCapacity), result.UploadCapacityAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Download Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.DownloadRPM), result.DownloadRPMAccuracy)
|
||||
fmt.Fprintf(os.Stderr, "Upload Responsiveness: %-20s Accuracy: %s\n", fmt.Sprintf("%d RPM", result.UploadRPM), result.UploadRPMAccuracy)
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatBitrate(bps int64) string {
|
||||
switch {
|
||||
case bps >= 1_000_000_000:
|
||||
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
|
||||
case bps >= 1_000_000:
|
||||
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
|
||||
case bps >= 1_000:
|
||||
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
|
||||
default:
|
||||
return fmt.Sprintf("%d bps", bps)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
"github.com/sagernet/sing-box/log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var commandSTUNFlagServer string
|
||||
|
||||
var commandSTUN = &cobra.Command{
|
||||
Use: "stun",
|
||||
Short: "Run a STUN test",
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := runSTUN()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
commandSTUN.Flags().StringVarP(&commandSTUNFlagServer, "server", "s", stun.DefaultServer, "STUN server address")
|
||||
commandTools.AddCommand(commandSTUN)
|
||||
}
|
||||
|
||||
func runSTUN() error {
|
||||
instance, err := createPreStartedClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer instance.Close()
|
||||
|
||||
dialer, err := createDialer(instance, commandToolsFlagOutbound)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "==== STUN TEST ====")
|
||||
|
||||
result, err := stun.Run(stun.Options{
|
||||
Server: commandSTUNFlagServer,
|
||||
Dialer: dialer,
|
||||
Context: globalCtx,
|
||||
OnProgress: func(p stun.Progress) {
|
||||
switch p.Phase {
|
||||
case stun.PhaseBinding:
|
||||
if p.ExternalAddr != "" {
|
||||
fmt.Fprintf(os.Stderr, "\rExternal Address: %s (%d ms)", p.ExternalAddr, p.LatencyMs)
|
||||
} else {
|
||||
fmt.Fprint(os.Stderr, "\rSending binding request...")
|
||||
}
|
||||
case stun.PhaseNATMapping:
|
||||
fmt.Fprint(os.Stderr, "\rDetecting NAT mapping behavior...")
|
||||
case stun.PhaseNATFiltering:
|
||||
fmt.Fprint(os.Stderr, "\rDetecting NAT filtering behavior...")
|
||||
}
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr)
|
||||
fmt.Fprintf(os.Stderr, "External Address: %s\n", result.ExternalAddr)
|
||||
fmt.Fprintf(os.Stderr, "Latency: %d ms\n", result.LatencyMs)
|
||||
if result.NATTypeSupported {
|
||||
fmt.Fprintf(os.Stderr, "NAT Mapping: %s\n", result.NATMapping)
|
||||
fmt.Fprintf(os.Stderr, "NAT Filtering: %s\n", result.NATFiltering)
|
||||
} else {
|
||||
fmt.Fprintln(os.Stderr, "NAT Type Detection: not supported by server")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -149,7 +149,10 @@ func NewDefault(ctx context.Context, options option.DialerOptions) (*DefaultDial
|
||||
} else {
|
||||
dialer.Timeout = C.TCPConnectTimeout
|
||||
}
|
||||
if !options.DisableTCPKeepAlive {
|
||||
if options.DisableTCPKeepAlive {
|
||||
dialer.KeepAlive = -1
|
||||
dialer.KeepAliveConfig.Enable = false
|
||||
} else {
|
||||
keepIdle := time.Duration(options.TCPKeepAlive)
|
||||
if keepIdle == 0 {
|
||||
keepIdle = C.TCPKeepAliveInitial
|
||||
|
||||
@@ -37,7 +37,10 @@ func (l *Listener) ListenTCP() (net.Listener, error) {
|
||||
if l.listenOptions.ReuseAddr {
|
||||
listenConfig.Control = control.Append(listenConfig.Control, control.ReuseAddr())
|
||||
}
|
||||
if !l.listenOptions.DisableTCPKeepAlive {
|
||||
if l.listenOptions.DisableTCPKeepAlive {
|
||||
listenConfig.KeepAlive = -1
|
||||
listenConfig.KeepAliveConfig.Enable = false
|
||||
} else {
|
||||
keepIdle := time.Duration(l.listenOptions.TCPKeepAlive)
|
||||
if keepIdle == 0 {
|
||||
keepIdle = C.TCPKeepAliveInitial
|
||||
|
||||
@@ -2,6 +2,7 @@ package networkquality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -13,8 +14,19 @@ import (
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
// NewHTTPClient creates an http.Client that dials through the given dialer.
|
||||
// The dialer should already handle DNS resolution if needed.
|
||||
func FormatBitrate(bps int64) string {
|
||||
switch {
|
||||
case bps >= 1_000_000_000:
|
||||
return fmt.Sprintf("%.1f Gbps", float64(bps)/1_000_000_000)
|
||||
case bps >= 1_000_000:
|
||||
return fmt.Sprintf("%.1f Mbps", float64(bps)/1_000_000)
|
||||
case bps >= 1_000:
|
||||
return fmt.Sprintf("%.1f Kbps", float64(bps)/1_000)
|
||||
default:
|
||||
return fmt.Sprintf("%d bps", bps)
|
||||
}
|
||||
}
|
||||
|
||||
func NewHTTPClient(dialer N.Dialer) *http.Client {
|
||||
transport := &http.Transport{
|
||||
ForceAttemptHTTP2: true,
|
||||
@@ -67,7 +79,6 @@ func newMeasurementClient(
|
||||
dialer := &net.Dialer{}
|
||||
baseDialContext = dialer.DialContext
|
||||
}
|
||||
connectEndpoint = strings.TrimSpace(connectEndpoint)
|
||||
transport.DialContext = func(ctx context.Context, network string, addr string) (net.Conn, error) {
|
||||
dialAddr := addr
|
||||
if connectEndpoint != "" {
|
||||
@@ -91,7 +102,29 @@ func newMeasurementClient(
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MeasurementClientFactory func(
|
||||
connectEndpoint string,
|
||||
singleConnection bool,
|
||||
disableKeepAlives bool,
|
||||
readCounters []N.CountFunc,
|
||||
writeCounters []N.CountFunc,
|
||||
) (*http.Client, error)
|
||||
|
||||
func defaultMeasurementClientFactory(baseClient *http.Client) MeasurementClientFactory {
|
||||
return func(connectEndpoint string, singleConnection, disableKeepAlives bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
||||
return newMeasurementClient(baseClient, connectEndpoint, singleConnection, disableKeepAlives, readCounters, writeCounters)
|
||||
}
|
||||
}
|
||||
|
||||
func NewOptionalHTTP3Factory(dialer N.Dialer, useHTTP3 bool) (MeasurementClientFactory, error) {
|
||||
if !useHTTP3 {
|
||||
return nil, nil
|
||||
}
|
||||
return NewHTTP3MeasurementClientFactory(dialer)
|
||||
}
|
||||
|
||||
func rewriteDialAddress(addr string, connectEndpoint string) string {
|
||||
connectEndpoint = strings.TrimSpace(connectEndpoint)
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return addr
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
//go:build with_quic
|
||||
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/sagernet/quic-go"
|
||||
"github.com/sagernet/quic-go/http3"
|
||||
sBufio "github.com/sagernet/sing/common/bufio"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
||||
// singleConnection and disableKeepAlives are not applied:
|
||||
// HTTP/3 multiplexes streams over a single QUIC connection by default.
|
||||
return func(connectEndpoint string, _, _ bool, readCounters, writeCounters []N.CountFunc) (*http.Client, error) {
|
||||
transport := &http3.Transport{
|
||||
Dial: func(ctx context.Context, addr string, tlsCfg *tls.Config, cfg *quic.Config) (*quic.Conn, error) {
|
||||
dialAddr := addr
|
||||
if connectEndpoint != "" {
|
||||
dialAddr = rewriteDialAddress(addr, connectEndpoint)
|
||||
}
|
||||
destination := M.ParseSocksaddr(dialAddr)
|
||||
var udpConn net.Conn
|
||||
var dialErr error
|
||||
if dialer != nil {
|
||||
udpConn, dialErr = dialer.DialContext(ctx, N.NetworkUDP, destination)
|
||||
} else {
|
||||
var netDialer net.Dialer
|
||||
udpConn, dialErr = netDialer.DialContext(ctx, N.NetworkUDP, destination.String())
|
||||
}
|
||||
if dialErr != nil {
|
||||
return nil, dialErr
|
||||
}
|
||||
wrappedConn := udpConn
|
||||
if len(readCounters) > 0 || len(writeCounters) > 0 {
|
||||
wrappedConn = sBufio.NewCounterConn(udpConn, readCounters, writeCounters)
|
||||
}
|
||||
packetConn := sBufio.NewUnbindPacketConn(wrappedConn)
|
||||
quicConn, dialErr := quic.DialEarly(ctx, packetConn, udpConn.RemoteAddr(), tlsCfg, cfg)
|
||||
if dialErr != nil {
|
||||
udpConn.Close()
|
||||
return nil, dialErr
|
||||
}
|
||||
return quicConn, nil
|
||||
},
|
||||
}
|
||||
return &http.Client{Transport: transport}, nil
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//go:build !with_quic
|
||||
|
||||
package networkquality
|
||||
|
||||
import (
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
func NewHTTP3MeasurementClientFactory(dialer N.Dialer) (MeasurementClientFactory, error) {
|
||||
return nil, C.ErrQUICNotIncluded
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
@@ -113,12 +114,13 @@ const (
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
ConfigURL string
|
||||
HTTPClient *http.Client
|
||||
Serial bool
|
||||
MaxRuntime time.Duration
|
||||
OnProgress func(Progress)
|
||||
Context context.Context
|
||||
ConfigURL string
|
||||
HTTPClient *http.Client
|
||||
NewMeasurementClient MeasurementClientFactory
|
||||
Serial bool
|
||||
MaxRuntime time.Duration
|
||||
OnProgress func(Progress)
|
||||
Context context.Context
|
||||
}
|
||||
|
||||
const DefaultMaxRuntime = 20 * time.Second
|
||||
@@ -129,14 +131,13 @@ type measurementSettings struct {
|
||||
stabilityInterval time.Duration
|
||||
sampleInterval time.Duration
|
||||
progressInterval time.Duration
|
||||
baseProbeInterval time.Duration
|
||||
maxProbesPerSecond int
|
||||
initialConnections int
|
||||
maxConnections int
|
||||
movingAvgDistance int
|
||||
trimPercent int
|
||||
stdDevTolerancePct float64
|
||||
maxProbeCapacityPct float64
|
||||
uploadRequestSize int64
|
||||
}
|
||||
|
||||
var settings = measurementSettings{
|
||||
@@ -145,14 +146,13 @@ var settings = measurementSettings{
|
||||
stabilityInterval: time.Second,
|
||||
sampleInterval: 250 * time.Millisecond,
|
||||
progressInterval: 500 * time.Millisecond,
|
||||
baseProbeInterval: 100 * time.Millisecond,
|
||||
maxProbesPerSecond: 100,
|
||||
initialConnections: 1,
|
||||
maxConnections: 16,
|
||||
movingAvgDistance: 4,
|
||||
trimPercent: 5,
|
||||
stdDevTolerancePct: 5,
|
||||
maxProbeCapacityPct: 0.05,
|
||||
uploadRequestSize: 4 << 20,
|
||||
}
|
||||
|
||||
type resolvedConfig struct {
|
||||
@@ -175,6 +175,7 @@ type probeTrace struct {
|
||||
connectDone time.Time
|
||||
tlsStart time.Time
|
||||
tlsDone time.Time
|
||||
tlsVersion uint16
|
||||
gotConn time.Time
|
||||
wroteRequest time.Time
|
||||
firstResponseByte time.Time
|
||||
@@ -273,7 +274,6 @@ type intervalWindow struct {
|
||||
|
||||
type stabilityTracker struct {
|
||||
window int
|
||||
trimPercent int
|
||||
stdDevTolerancePct float64
|
||||
instantaneous []float64
|
||||
movingAverages []float64
|
||||
@@ -312,15 +312,11 @@ func (s *stabilityTracker) stable() bool {
|
||||
if len(s.movingAverages) < s.window {
|
||||
return false
|
||||
}
|
||||
trimmed := upperTrimFloat64s(s.movingAverages, s.trimPercent)
|
||||
if len(trimmed) == 0 {
|
||||
currentAverage := s.movingAverages[len(s.movingAverages)-1]
|
||||
if currentAverage <= 0 {
|
||||
return false
|
||||
}
|
||||
mean := meanFloat64s(trimmed)
|
||||
if mean <= 0 {
|
||||
return false
|
||||
}
|
||||
return stdDevFloat64s(trimmed) <= mean*(s.stdDevTolerancePct/100)
|
||||
return stdDevFloat64s(s.movingAverages) <= currentAverage*(s.stdDevTolerancePct/100)
|
||||
}
|
||||
|
||||
type directionMeasurement struct {
|
||||
@@ -331,7 +327,7 @@ type directionMeasurement struct {
|
||||
}
|
||||
|
||||
type directionRunner struct {
|
||||
baseClient *http.Client
|
||||
factory MeasurementClientFactory
|
||||
plan directionPlan
|
||||
probeBytes int64
|
||||
|
||||
@@ -344,9 +340,8 @@ type directionRunner struct {
|
||||
currentRPM atomic.Int32
|
||||
currentInterval atomic.Int64
|
||||
|
||||
connMu sync.Mutex
|
||||
connections []*loadConnection
|
||||
nextConnection int
|
||||
connMu sync.Mutex
|
||||
connections []*loadConnection
|
||||
|
||||
probeMu sync.Mutex
|
||||
probeRounds []probeRound
|
||||
@@ -356,9 +351,9 @@ type directionRunner struct {
|
||||
throughputWindow *intervalWindow
|
||||
}
|
||||
|
||||
func newDirectionRunner(baseClient *http.Client, plan directionPlan, probeBytes int64) *directionRunner {
|
||||
func newDirectionRunner(factory MeasurementClientFactory, plan directionPlan, probeBytes int64) *directionRunner {
|
||||
return &directionRunner{
|
||||
baseClient: baseClient,
|
||||
factory: factory,
|
||||
plan: plan,
|
||||
probeBytes: probeBytes,
|
||||
errCh: make(chan error, 1),
|
||||
@@ -399,7 +394,7 @@ func (r *directionRunner) addConnection(ctx context.Context) error {
|
||||
} else {
|
||||
readCounters = []N.CountFunc{counter}
|
||||
}
|
||||
client, err := newMeasurementClient(r.baseClient, r.plan.connectEndpoint, true, false, readCounters, writeCounters)
|
||||
client, err := r.factory(r.plan.connectEndpoint, true, false, readCounters, writeCounters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -428,45 +423,42 @@ func (r *directionRunner) connectionCount() int {
|
||||
func (r *directionRunner) pickReadyConnection() *loadConnection {
|
||||
r.connMu.Lock()
|
||||
defer r.connMu.Unlock()
|
||||
if len(r.connections) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := 0; i < len(r.connections); i++ {
|
||||
index := (r.nextConnection + i) % len(r.connections)
|
||||
if r.connections[index].ready.Load() && r.connections[index].active.Load() {
|
||||
r.nextConnection = (index + 1) % len(r.connections)
|
||||
return r.connections[index]
|
||||
var ready []*loadConnection
|
||||
for _, conn := range r.connections {
|
||||
if conn.ready.Load() && conn.active.Load() {
|
||||
ready = append(ready, conn)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if len(ready) == 0 {
|
||||
return nil
|
||||
}
|
||||
return ready[rand.Intn(len(ready))]
|
||||
}
|
||||
|
||||
func (r *directionRunner) startProber(ctx context.Context) {
|
||||
r.wg.Add(1)
|
||||
go func() {
|
||||
defer r.wg.Done()
|
||||
timer := time.NewTimer(0)
|
||||
defer timer.Stop()
|
||||
ticker := time.NewTicker(r.probeInterval())
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-timer.C:
|
||||
case <-ticker.C:
|
||||
}
|
||||
conn := r.pickReadyConnection()
|
||||
if conn != nil {
|
||||
foreignClient, err := newMeasurementClient(r.baseClient, r.plan.connectEndpoint, true, true, nil, nil)
|
||||
if conn == nil {
|
||||
continue
|
||||
}
|
||||
go func(selfClient *http.Client) {
|
||||
foreignClient, err := r.factory(r.plan.connectEndpoint, true, true, nil, nil)
|
||||
if err != nil {
|
||||
r.fail(err)
|
||||
return
|
||||
}
|
||||
round, err := collectProbeRound(ctx, foreignClient, conn.client, r.plan.probeURL.String())
|
||||
round, err := collectProbeRound(ctx, foreignClient, selfClient, r.plan.probeURL.String())
|
||||
foreignClient.CloseIdleConnections()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
r.fail(err)
|
||||
return
|
||||
}
|
||||
r.recordProbeRound(probeRound{
|
||||
@@ -476,14 +468,14 @@ func (r *directionRunner) startProber(ctx context.Context) {
|
||||
httpFirst: round.httpFirst,
|
||||
httpLoaded: round.httpLoaded,
|
||||
})
|
||||
}
|
||||
timer.Reset(r.currentProbeInterval())
|
||||
}(conn.client)
|
||||
ticker.Reset(r.probeInterval())
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (r *directionRunner) currentProbeInterval() time.Duration {
|
||||
interval := settings.baseProbeInterval
|
||||
func (r *directionRunner) probeInterval() time.Duration {
|
||||
interval := time.Second / time.Duration(settings.maxProbesPerSecond)
|
||||
capacity := r.currentCapacity.Load()
|
||||
if capacity <= 0 || r.probeBytes <= 0 || settings.maxProbeCapacityPct <= 0 {
|
||||
return interval
|
||||
@@ -497,9 +489,6 @@ func (r *directionRunner) currentProbeInterval() time.Duration {
|
||||
if capacityInterval > interval {
|
||||
interval = capacityInterval
|
||||
}
|
||||
if interval > settings.stabilityInterval {
|
||||
interval = settings.stabilityInterval
|
||||
}
|
||||
return interval
|
||||
}
|
||||
|
||||
@@ -625,18 +614,25 @@ func Run(options Options) (*Result, error) {
|
||||
options.OnProgress(progress)
|
||||
}
|
||||
|
||||
factory := options.NewMeasurementClient
|
||||
if factory == nil {
|
||||
factory = defaultMeasurementClientFactory(options.HTTPClient)
|
||||
}
|
||||
|
||||
report(Progress{Phase: PhaseIdle})
|
||||
idleLatency, probeBytes, err := measureIdleLatency(ctx, options.HTTPClient, resolved)
|
||||
idleLatency, probeBytes, err := measureIdleLatency(ctx, factory, resolved)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "measure idle latency")
|
||||
}
|
||||
report(Progress{Phase: PhaseIdle, IdleLatencyMs: idleLatency})
|
||||
|
||||
start = time.Now()
|
||||
|
||||
var download, upload *directionMeasurement
|
||||
if options.Serial {
|
||||
download, upload, err = measureSerial(
|
||||
ctx,
|
||||
options.HTTPClient,
|
||||
factory,
|
||||
resolved,
|
||||
idleLatency,
|
||||
probeBytes,
|
||||
@@ -646,7 +642,7 @@ func Run(options Options) (*Result, error) {
|
||||
} else {
|
||||
download, upload, err = measureParallel(
|
||||
ctx,
|
||||
options.HTTPClient,
|
||||
factory,
|
||||
resolved,
|
||||
idleLatency,
|
||||
probeBytes,
|
||||
@@ -696,7 +692,7 @@ func normalizeMaxRuntime(maxRuntime time.Duration) (time.Duration, error) {
|
||||
|
||||
func measureSerial(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
factory MeasurementClientFactory,
|
||||
resolved *resolvedConfig,
|
||||
idleLatency int32,
|
||||
probeBytes int64,
|
||||
@@ -705,7 +701,7 @@ func measureSerial(
|
||||
) (*directionMeasurement, *directionMeasurement, error) {
|
||||
downloadRuntime, uploadRuntime := splitRuntimeBudget(maxRuntime, 2)
|
||||
report(Progress{Phase: PhaseDownload, IdleLatencyMs: idleLatency})
|
||||
download, err := measureDirection(ctx, client, directionPlan{
|
||||
download, err := measureDirection(ctx, factory, directionPlan{
|
||||
dataURL: resolved.largeURL,
|
||||
probeURL: resolved.smallURL,
|
||||
connectEndpoint: resolved.connectEndpoint,
|
||||
@@ -727,7 +723,7 @@ func measureSerial(
|
||||
DownloadRPM: download.rpm,
|
||||
IdleLatencyMs: idleLatency,
|
||||
})
|
||||
upload, err := measureDirection(ctx, client, directionPlan{
|
||||
upload, err := measureDirection(ctx, factory, directionPlan{
|
||||
dataURL: resolved.uploadURL,
|
||||
probeURL: resolved.smallURL,
|
||||
connectEndpoint: resolved.connectEndpoint,
|
||||
@@ -750,7 +746,7 @@ func measureSerial(
|
||||
|
||||
func measureParallel(
|
||||
ctx context.Context,
|
||||
client *http.Client,
|
||||
factory MeasurementClientFactory,
|
||||
resolved *resolvedConfig,
|
||||
idleLatency int32,
|
||||
probeBytes int64,
|
||||
@@ -803,7 +799,7 @@ func measureParallel(
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
measurement, err := measureDirection(parallelCtx, client, directionPlan{
|
||||
measurement, err := measureDirection(parallelCtx, factory, directionPlan{
|
||||
dataURL: resolved.largeURL,
|
||||
probeURL: resolved.smallURL,
|
||||
connectEndpoint: resolved.connectEndpoint,
|
||||
@@ -819,7 +815,7 @@ func measureParallel(
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
measurement, err := measureDirection(parallelCtx, client, directionPlan{
|
||||
measurement, err := measureDirection(parallelCtx, factory, directionPlan{
|
||||
dataURL: resolved.uploadURL,
|
||||
probeURL: resolved.smallURL,
|
||||
connectEndpoint: resolved.connectEndpoint,
|
||||
@@ -935,7 +931,7 @@ func validateConfig(config *Config) (*resolvedConfig, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func measureIdleLatency(ctx context.Context, baseClient *http.Client, config *resolvedConfig) (int32, int64, error) {
|
||||
func measureIdleLatency(ctx context.Context, factory MeasurementClientFactory, config *resolvedConfig) (int32, int64, error) {
|
||||
var latencies []int64
|
||||
var maxProbeBytes int64
|
||||
for i := 0; i < settings.idleProbeCount; i++ {
|
||||
@@ -944,7 +940,7 @@ func measureIdleLatency(ctx context.Context, baseClient *http.Client, config *re
|
||||
return 0, 0, ctx.Err()
|
||||
default:
|
||||
}
|
||||
client, err := newMeasurementClient(baseClient, config.connectEndpoint, true, true, nil, nil)
|
||||
client, err := factory(config.connectEndpoint, true, true, nil, nil)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@@ -964,7 +960,7 @@ func measureIdleLatency(ctx context.Context, baseClient *http.Client, config *re
|
||||
|
||||
func measureDirection(
|
||||
ctx context.Context,
|
||||
baseClient *http.Client,
|
||||
factory MeasurementClientFactory,
|
||||
plan directionPlan,
|
||||
probeBytes int64,
|
||||
maxRuntime time.Duration,
|
||||
@@ -973,7 +969,7 @@ func measureDirection(
|
||||
phaseCtx, phaseCancel := context.WithTimeout(ctx, maxRuntime)
|
||||
defer phaseCancel()
|
||||
|
||||
runner := newDirectionRunner(baseClient, plan, probeBytes)
|
||||
runner := newDirectionRunner(factory, plan, probeBytes)
|
||||
defer runner.wait()
|
||||
|
||||
for i := 0; i < settings.initialConnections; i++ {
|
||||
@@ -983,14 +979,14 @@ func measureDirection(
|
||||
}
|
||||
}
|
||||
|
||||
runner.startProber(phaseCtx)
|
||||
|
||||
throughputTracker := stabilityTracker{
|
||||
window: settings.movingAvgDistance,
|
||||
trimPercent: settings.trimPercent,
|
||||
stdDevTolerancePct: settings.stdDevTolerancePct,
|
||||
}
|
||||
responsivenessTracker := stabilityTracker{
|
||||
window: settings.movingAvgDistance,
|
||||
trimPercent: settings.trimPercent,
|
||||
stdDevTolerancePct: settings.stdDevTolerancePct,
|
||||
}
|
||||
|
||||
@@ -1007,7 +1003,7 @@ func measureDirection(
|
||||
prevIntervalBytes := int64(0)
|
||||
prevIntervalTime := start
|
||||
var ewmaCapacity float64
|
||||
var proberStarted bool
|
||||
var goodputSaturated bool
|
||||
var intervalIndex int
|
||||
|
||||
for {
|
||||
@@ -1039,18 +1035,17 @@ func measureDirection(
|
||||
if throughputStable && runner.throughputWindow == nil {
|
||||
runner.setThroughputWindow(intervalIndex)
|
||||
}
|
||||
if !proberStarted && (throughputStable || (runner.connectionCount() >= settings.maxConnections && throughputTracker.ready())) {
|
||||
proberStarted = true
|
||||
runner.startProber(phaseCtx)
|
||||
if !goodputSaturated && (throughputStable || (runner.connectionCount() >= settings.maxConnections && throughputTracker.ready())) {
|
||||
goodputSaturated = true
|
||||
}
|
||||
if runner.connectionCount() < settings.maxConnections && runner.throughputWindow == nil {
|
||||
if runner.connectionCount() < settings.maxConnections {
|
||||
err := runner.addConnection(phaseCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if proberStarted {
|
||||
if goodputSaturated {
|
||||
if values := runner.swapIntervalProbeValues(); len(values) > 0 {
|
||||
if responsivenessTracker.add(upperTrimmedMean(values, settings.trimPercent)) && runner.responsivenessWindow == nil {
|
||||
runner.setResponsivenessWindow(intervalIndex)
|
||||
@@ -1141,9 +1136,10 @@ func runProbe(ctx context.Context, client *http.Client, rawURL string, expectReu
|
||||
trace.tlsStart = time.Now()
|
||||
}
|
||||
},
|
||||
TLSHandshakeDone: func(tls.ConnectionState, error) {
|
||||
TLSHandshakeDone: func(state tls.ConnectionState, _ error) {
|
||||
if trace.tlsDone.IsZero() {
|
||||
trace.tlsDone = time.Now()
|
||||
trace.tlsVersion = state.Version
|
||||
}
|
||||
},
|
||||
GotConn: func(info httptrace.GotConnInfo) {
|
||||
@@ -1204,6 +1200,9 @@ func runProbe(ctx context.Context, client *http.Client, rawURL string, expectReu
|
||||
}
|
||||
if !trace.tlsStart.IsZero() && !trace.tlsDone.IsZero() && trace.tlsDone.After(trace.tlsStart) {
|
||||
measurement.tls = trace.tlsDone.Sub(trace.tlsStart)
|
||||
if roundTrips := tlsHandshakeRoundTrips(trace.tlsVersion); roundTrips > 1 {
|
||||
measurement.tls /= time.Duration(roundTrips)
|
||||
}
|
||||
}
|
||||
if !trace.firstResponseByte.IsZero() && trace.firstResponseByte.After(httpStart) {
|
||||
measurement.httpFirst = trace.firstResponseByte.Sub(httpStart)
|
||||
@@ -1240,17 +1239,20 @@ func runDownloadRequest(ctx context.Context, client *http.Client, rawURL string,
|
||||
|
||||
func runUploadRequest(ctx context.Context, client *http.Client, rawURL string, onActive func()) error {
|
||||
body := &uploadBody{
|
||||
remaining: settings.uploadRequestSize,
|
||||
onActive: onActive,
|
||||
ctx: ctx,
|
||||
onActive: onActive,
|
||||
}
|
||||
req, err := newRequest(ctx, http.MethodPost, rawURL, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = settings.uploadRequestSize
|
||||
req.ContentLength = -1
|
||||
req.Header.Set("Content-Type", "application/octet-stream")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -1258,8 +1260,9 @@ func runUploadRequest(ctx context.Context, client *http.Client, rawURL string, o
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(io.Discard, resp.Body)
|
||||
return err
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
<-ctx.Done()
|
||||
return nil
|
||||
}
|
||||
|
||||
func newRequest(ctx context.Context, method string, rawURL string, body io.Reader) (*http.Request, error) {
|
||||
@@ -1326,6 +1329,15 @@ func calculateRPM(rounds []probeRound) int32 {
|
||||
return int32(math.Round((foreignRPM + loadedRPM) / 2))
|
||||
}
|
||||
|
||||
func tlsHandshakeRoundTrips(version uint16) int {
|
||||
switch version {
|
||||
case tls.VersionTLS12, tls.VersionTLS11, tls.VersionTLS10:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func durationMillis(value time.Duration) float64 {
|
||||
return float64(value) / float64(time.Millisecond)
|
||||
}
|
||||
@@ -1379,27 +1391,20 @@ func stdDevFloat64s(values []float64) float64 {
|
||||
}
|
||||
|
||||
type uploadBody struct {
|
||||
remaining int64
|
||||
ctx context.Context
|
||||
activated atomic.Bool
|
||||
onActive func()
|
||||
}
|
||||
|
||||
func (u *uploadBody) Read(p []byte) (int, error) {
|
||||
if u.remaining == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
if int64(len(p)) > u.remaining {
|
||||
p = p[:int(u.remaining)]
|
||||
if err := u.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
clear(p)
|
||||
n := len(p)
|
||||
if n > 0 && u.onActive != nil && u.activated.CompareAndSwap(false, true) {
|
||||
u.onActive()
|
||||
}
|
||||
u.remaining -= int64(n)
|
||||
if u.remaining == 0 {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ const (
|
||||
ruleItemNetworkIsConstrained
|
||||
ruleItemNetworkInterfaceAddress
|
||||
ruleItemDefaultInterfaceAddress
|
||||
ruleItemPackageNameRegex
|
||||
ruleItemFinal uint8 = 0xFF
|
||||
)
|
||||
|
||||
@@ -215,6 +216,8 @@ func readDefaultRule(reader varbin.Reader, recover bool) (rule option.DefaultHea
|
||||
rule.ProcessPathRegex, err = readRuleItemString(reader)
|
||||
case ruleItemPackageName:
|
||||
rule.PackageName, err = readRuleItemString(reader)
|
||||
case ruleItemPackageNameRegex:
|
||||
rule.PackageNameRegex, err = readRuleItemString(reader)
|
||||
case ruleItemWIFISSID:
|
||||
rule.WIFISSID, err = readRuleItemString(reader)
|
||||
case ruleItemWIFIBSSID:
|
||||
@@ -394,6 +397,15 @@ func writeDefaultRule(writer varbin.Writer, rule option.DefaultHeadlessRule, gen
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.PackageNameRegex) > 0 {
|
||||
if generateVersion < C.RuleSetVersion5 {
|
||||
return E.New("`package_name_regex` rule item is only supported in version 5 or later")
|
||||
}
|
||||
err = writeRuleItemString(writer, ruleItemPackageNameRegex, rule.PackageNameRegex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(rule.NetworkType) > 0 {
|
||||
if generateVersion < C.RuleSetVersion3 {
|
||||
return E.New("`network_type` rule item is only supported in version 3 or later")
|
||||
|
||||
@@ -0,0 +1,607 @@
|
||||
package stun
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultServer = "stun.voipgate.com:3478"
|
||||
|
||||
magicCookie = 0x2112A442
|
||||
headerSize = 20
|
||||
|
||||
bindingRequest = 0x0001
|
||||
bindingSuccessResponse = 0x0101
|
||||
bindingErrorResponse = 0x0111
|
||||
|
||||
attrMappedAddress = 0x0001
|
||||
attrChangeRequest = 0x0003
|
||||
attrErrorCode = 0x0009
|
||||
attrXORMappedAddress = 0x0020
|
||||
attrOtherAddress = 0x802c
|
||||
|
||||
familyIPv4 = 0x01
|
||||
familyIPv6 = 0x02
|
||||
|
||||
changeIP = 0x04
|
||||
changePort = 0x02
|
||||
|
||||
defaultRTO = 500 * time.Millisecond
|
||||
minRTO = 250 * time.Millisecond
|
||||
maxRetransmit = 2
|
||||
)
|
||||
|
||||
type Phase int32
|
||||
|
||||
const (
|
||||
PhaseBinding Phase = iota
|
||||
PhaseNATMapping
|
||||
PhaseNATFiltering
|
||||
PhaseDone
|
||||
)
|
||||
|
||||
type NATMapping int32
|
||||
|
||||
const (
|
||||
NATMappingUnknown NATMapping = iota
|
||||
_ // reserved
|
||||
NATMappingEndpointIndependent
|
||||
NATMappingAddressDependent
|
||||
NATMappingAddressAndPortDependent
|
||||
)
|
||||
|
||||
func (m NATMapping) String() string {
|
||||
switch m {
|
||||
case NATMappingEndpointIndependent:
|
||||
return "Endpoint Independent"
|
||||
case NATMappingAddressDependent:
|
||||
return "Address Dependent"
|
||||
case NATMappingAddressAndPortDependent:
|
||||
return "Address and Port Dependent"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type NATFiltering int32
|
||||
|
||||
const (
|
||||
NATFilteringUnknown NATFiltering = iota
|
||||
NATFilteringEndpointIndependent
|
||||
NATFilteringAddressDependent
|
||||
NATFilteringAddressAndPortDependent
|
||||
)
|
||||
|
||||
func (f NATFiltering) String() string {
|
||||
switch f {
|
||||
case NATFilteringEndpointIndependent:
|
||||
return "Endpoint Independent"
|
||||
case NATFilteringAddressDependent:
|
||||
return "Address Dependent"
|
||||
case NATFilteringAddressAndPortDependent:
|
||||
return "Address and Port Dependent"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
type TransactionID [12]byte
|
||||
|
||||
type Options struct {
|
||||
Server string
|
||||
Dialer N.Dialer
|
||||
Context context.Context
|
||||
OnProgress func(Progress)
|
||||
}
|
||||
|
||||
type Progress struct {
|
||||
Phase Phase
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping NATMapping
|
||||
NATFiltering NATFiltering
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
ExternalAddr string
|
||||
LatencyMs int32
|
||||
NATMapping NATMapping
|
||||
NATFiltering NATFiltering
|
||||
NATTypeSupported bool
|
||||
}
|
||||
|
||||
type parsedResponse struct {
|
||||
xorMappedAddr netip.AddrPort
|
||||
mappedAddr netip.AddrPort
|
||||
otherAddr netip.AddrPort
|
||||
}
|
||||
|
||||
func (r *parsedResponse) externalAddr() (netip.AddrPort, bool) {
|
||||
if r.xorMappedAddr.IsValid() {
|
||||
return r.xorMappedAddr, true
|
||||
}
|
||||
if r.mappedAddr.IsValid() {
|
||||
return r.mappedAddr, true
|
||||
}
|
||||
return netip.AddrPort{}, false
|
||||
}
|
||||
|
||||
type stunAttribute struct {
|
||||
typ uint16
|
||||
value []byte
|
||||
}
|
||||
|
||||
func newTransactionID() TransactionID {
|
||||
var id TransactionID
|
||||
_, _ = rand.Read(id[:])
|
||||
return id
|
||||
}
|
||||
|
||||
func buildBindingRequest(txID TransactionID, attrs ...stunAttribute) []byte {
|
||||
attrLen := 0
|
||||
for _, attr := range attrs {
|
||||
attrLen += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||
}
|
||||
|
||||
buf := make([]byte, headerSize+attrLen)
|
||||
binary.BigEndian.PutUint16(buf[0:2], bindingRequest)
|
||||
binary.BigEndian.PutUint16(buf[2:4], uint16(attrLen))
|
||||
binary.BigEndian.PutUint32(buf[4:8], magicCookie)
|
||||
copy(buf[8:20], txID[:])
|
||||
|
||||
offset := headerSize
|
||||
for _, attr := range attrs {
|
||||
binary.BigEndian.PutUint16(buf[offset:offset+2], attr.typ)
|
||||
binary.BigEndian.PutUint16(buf[offset+2:offset+4], uint16(len(attr.value)))
|
||||
copy(buf[offset+4:offset+4+len(attr.value)], attr.value)
|
||||
offset += 4 + len(attr.value) + paddingLen(len(attr.value))
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
func changeRequestAttr(flags byte) stunAttribute {
|
||||
return stunAttribute{
|
||||
typ: attrChangeRequest,
|
||||
value: []byte{0, 0, 0, flags},
|
||||
}
|
||||
}
|
||||
|
||||
func parseResponse(data []byte, expectedTxID TransactionID) (*parsedResponse, error) {
|
||||
if len(data) < headerSize {
|
||||
return nil, E.New("response too short")
|
||||
}
|
||||
|
||||
msgType := binary.BigEndian.Uint16(data[0:2])
|
||||
if msgType&0xC000 != 0 {
|
||||
return nil, E.New("invalid STUN message: top 2 bits not zero")
|
||||
}
|
||||
|
||||
cookie := binary.BigEndian.Uint32(data[4:8])
|
||||
if cookie != magicCookie {
|
||||
return nil, E.New("invalid magic cookie")
|
||||
}
|
||||
|
||||
var txID TransactionID
|
||||
copy(txID[:], data[8:20])
|
||||
if txID != expectedTxID {
|
||||
return nil, E.New("transaction ID mismatch")
|
||||
}
|
||||
|
||||
msgLen := int(binary.BigEndian.Uint16(data[2:4]))
|
||||
if msgLen > len(data)-headerSize {
|
||||
return nil, E.New("message length exceeds data")
|
||||
}
|
||||
|
||||
attrData := data[headerSize : headerSize+msgLen]
|
||||
|
||||
if msgType == bindingErrorResponse {
|
||||
return nil, parseErrorResponse(attrData)
|
||||
}
|
||||
if msgType != bindingSuccessResponse {
|
||||
return nil, E.New("unexpected message type: ", fmt.Sprintf("0x%04x", msgType))
|
||||
}
|
||||
|
||||
resp := &parsedResponse{}
|
||||
offset := 0
|
||||
for offset+4 <= len(attrData) {
|
||||
attrType := binary.BigEndian.Uint16(attrData[offset : offset+2])
|
||||
attrLen := int(binary.BigEndian.Uint16(attrData[offset+2 : offset+4]))
|
||||
if offset+4+attrLen > len(attrData) {
|
||||
break
|
||||
}
|
||||
attrValue := attrData[offset+4 : offset+4+attrLen]
|
||||
|
||||
switch attrType {
|
||||
case attrXORMappedAddress:
|
||||
addr, err := parseXORMappedAddress(attrValue, txID)
|
||||
if err == nil {
|
||||
resp.xorMappedAddr = addr
|
||||
}
|
||||
case attrMappedAddress:
|
||||
addr, err := parseMappedAddress(attrValue)
|
||||
if err == nil {
|
||||
resp.mappedAddr = addr
|
||||
}
|
||||
case attrOtherAddress:
|
||||
addr, err := parseMappedAddress(attrValue)
|
||||
if err == nil {
|
||||
resp.otherAddr = addr
|
||||
}
|
||||
}
|
||||
|
||||
offset += 4 + attrLen + paddingLen(attrLen)
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func parseErrorResponse(data []byte) error {
|
||||
offset := 0
|
||||
for offset+4 <= len(data) {
|
||||
attrType := binary.BigEndian.Uint16(data[offset : offset+2])
|
||||
attrLen := int(binary.BigEndian.Uint16(data[offset+2 : offset+4]))
|
||||
if offset+4+attrLen > len(data) {
|
||||
break
|
||||
}
|
||||
if attrType == attrErrorCode && attrLen >= 4 {
|
||||
attrValue := data[offset+4 : offset+4+attrLen]
|
||||
class := int(attrValue[2] & 0x07)
|
||||
number := int(attrValue[3])
|
||||
code := class*100 + number
|
||||
if attrLen > 4 {
|
||||
return E.New("STUN error ", code, ": ", string(attrValue[4:]))
|
||||
}
|
||||
return E.New("STUN error ", code)
|
||||
}
|
||||
offset += 4 + attrLen + paddingLen(attrLen)
|
||||
}
|
||||
return E.New("STUN error response")
|
||||
}
|
||||
|
||||
func parseXORMappedAddress(data []byte, txID TransactionID) (netip.AddrPort, error) {
|
||||
if len(data) < 4 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS too short")
|
||||
}
|
||||
|
||||
family := data[1]
|
||||
xPort := binary.BigEndian.Uint16(data[2:4])
|
||||
port := xPort ^ uint16(magicCookie>>16)
|
||||
|
||||
switch family {
|
||||
case familyIPv4:
|
||||
if len(data) < 8 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv4 too short")
|
||||
}
|
||||
var ip [4]byte
|
||||
binary.BigEndian.PutUint32(ip[:], binary.BigEndian.Uint32(data[4:8])^magicCookie)
|
||||
return netip.AddrPortFrom(netip.AddrFrom4(ip), port), nil
|
||||
case familyIPv6:
|
||||
if len(data) < 20 {
|
||||
return netip.AddrPort{}, E.New("XOR-MAPPED-ADDRESS IPv6 too short")
|
||||
}
|
||||
var ip [16]byte
|
||||
var xorKey [16]byte
|
||||
binary.BigEndian.PutUint32(xorKey[0:4], magicCookie)
|
||||
copy(xorKey[4:16], txID[:])
|
||||
for i := range 16 {
|
||||
ip[i] = data[4+i] ^ xorKey[i]
|
||||
}
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||
default:
|
||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||
}
|
||||
}
|
||||
|
||||
func parseMappedAddress(data []byte) (netip.AddrPort, error) {
|
||||
if len(data) < 4 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS too short")
|
||||
}
|
||||
|
||||
family := data[1]
|
||||
port := binary.BigEndian.Uint16(data[2:4])
|
||||
|
||||
switch family {
|
||||
case familyIPv4:
|
||||
if len(data) < 8 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv4 too short")
|
||||
}
|
||||
return netip.AddrPortFrom(
|
||||
netip.AddrFrom4([4]byte{data[4], data[5], data[6], data[7]}), port,
|
||||
), nil
|
||||
case familyIPv6:
|
||||
if len(data) < 20 {
|
||||
return netip.AddrPort{}, E.New("MAPPED-ADDRESS IPv6 too short")
|
||||
}
|
||||
var ip [16]byte
|
||||
copy(ip[:], data[4:20])
|
||||
return netip.AddrPortFrom(netip.AddrFrom16(ip), port), nil
|
||||
default:
|
||||
return netip.AddrPort{}, E.New("unknown address family: ", family)
|
||||
}
|
||||
}
|
||||
|
||||
func roundTrip(conn net.PacketConn, addr net.Addr, txID TransactionID, attrs []stunAttribute, rto time.Duration) (*parsedResponse, time.Duration, error) {
|
||||
request := buildBindingRequest(txID, attrs...)
|
||||
currentRTO := rto
|
||||
retransmitCount := 0
|
||||
|
||||
sendTime := time.Now()
|
||||
_, err := conn.WriteTo(request, addr)
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "send STUN request")
|
||||
}
|
||||
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
err = conn.SetReadDeadline(sendTime.Add(currentRTO))
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "set read deadline")
|
||||
}
|
||||
|
||||
n, _, readErr := conn.ReadFrom(buf)
|
||||
if readErr != nil {
|
||||
if E.IsTimeout(readErr) && retransmitCount < maxRetransmit {
|
||||
retransmitCount++
|
||||
currentRTO *= 2
|
||||
sendTime = time.Now()
|
||||
_, err = conn.WriteTo(request, addr)
|
||||
if err != nil {
|
||||
return nil, 0, E.Cause(err, "retransmit STUN request")
|
||||
}
|
||||
continue
|
||||
}
|
||||
return nil, 0, E.Cause(readErr, "read STUN response")
|
||||
}
|
||||
|
||||
if n < headerSize || buf[0]&0xC0 != 0 ||
|
||||
binary.BigEndian.Uint32(buf[4:8]) != magicCookie {
|
||||
continue
|
||||
}
|
||||
var receivedTxID TransactionID
|
||||
copy(receivedTxID[:], buf[8:20])
|
||||
if receivedTxID != txID {
|
||||
continue
|
||||
}
|
||||
|
||||
latency := time.Since(sendTime)
|
||||
|
||||
resp, parseErr := parseResponse(buf[:n], txID)
|
||||
if parseErr != nil {
|
||||
return nil, 0, parseErr
|
||||
}
|
||||
|
||||
return resp, latency, nil
|
||||
}
|
||||
}
|
||||
|
||||
func Run(options Options) (*Result, error) {
|
||||
ctx := options.Context
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
server := options.Server
|
||||
if server == "" {
|
||||
server = DefaultServer
|
||||
}
|
||||
serverSocksaddr := M.ParseSocksaddr(server)
|
||||
if serverSocksaddr.Port == 0 {
|
||||
serverSocksaddr.Port = 3478
|
||||
}
|
||||
|
||||
reportProgress := options.OnProgress
|
||||
if reportProgress == nil {
|
||||
reportProgress = func(Progress) {}
|
||||
}
|
||||
|
||||
var (
|
||||
packetConn net.PacketConn
|
||||
serverAddr net.Addr
|
||||
err error
|
||||
)
|
||||
|
||||
if options.Dialer != nil {
|
||||
packetConn, err = options.Dialer.ListenPacket(ctx, serverSocksaddr)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create UDP socket")
|
||||
}
|
||||
serverAddr = serverSocksaddr
|
||||
} else {
|
||||
serverUDPAddr, resolveErr := net.ResolveUDPAddr("udp", serverSocksaddr.String())
|
||||
if resolveErr != nil {
|
||||
return nil, E.Cause(resolveErr, "resolve STUN server")
|
||||
}
|
||||
packetConn, err = net.ListenPacket("udp", "")
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "create UDP socket")
|
||||
}
|
||||
serverAddr = serverUDPAddr
|
||||
}
|
||||
defer func() {
|
||||
_ = packetConn.Close()
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
default:
|
||||
}
|
||||
|
||||
rto := defaultRTO
|
||||
|
||||
// Phase 1: Binding
|
||||
reportProgress(Progress{Phase: PhaseBinding})
|
||||
|
||||
txID := newTransactionID()
|
||||
resp, latency, err := roundTrip(packetConn, serverAddr, txID, nil, rto)
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "binding request")
|
||||
}
|
||||
|
||||
rto = max(minRTO, 3*latency)
|
||||
|
||||
externalAddr, ok := resp.externalAddr()
|
||||
if !ok {
|
||||
return nil, E.New("no mapped address in response")
|
||||
}
|
||||
|
||||
result := &Result{
|
||||
ExternalAddr: externalAddr.String(),
|
||||
LatencyMs: int32(latency.Milliseconds()),
|
||||
}
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseBinding,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
|
||||
otherAddr := resp.otherAddr
|
||||
if !otherAddr.IsValid() {
|
||||
result.NATTypeSupported = false
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseDone,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
result.NATTypeSupported = true
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Phase 2: NAT Mapping Detection (RFC 5780 Section 4.3)
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATMapping,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
})
|
||||
|
||||
result.NATMapping = detectNATMapping(
|
||||
packetConn, serverSocksaddr.Port, externalAddr, otherAddr, rto,
|
||||
)
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATMapping,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
})
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return result, nil
|
||||
default:
|
||||
}
|
||||
|
||||
// Phase 3: NAT Filtering Detection (RFC 5780 Section 4.4)
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseNATFiltering,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
})
|
||||
|
||||
result.NATFiltering = detectNATFiltering(packetConn, serverAddr, rto)
|
||||
|
||||
reportProgress(Progress{
|
||||
Phase: PhaseDone,
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NATMapping: result.NATMapping,
|
||||
NATFiltering: result.NATFiltering,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func detectNATMapping(
|
||||
conn net.PacketConn,
|
||||
serverPort uint16,
|
||||
externalAddr netip.AddrPort,
|
||||
otherAddr netip.AddrPort,
|
||||
rto time.Duration,
|
||||
) NATMapping {
|
||||
// Mapping Test II: Send to other_ip:server_port
|
||||
testIIAddr := net.UDPAddrFromAddrPort(
|
||||
netip.AddrPortFrom(otherAddr.Addr(), serverPort),
|
||||
)
|
||||
txID2 := newTransactionID()
|
||||
resp2, _, err := roundTrip(conn, testIIAddr, txID2, nil, rto)
|
||||
if err != nil {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
externalAddr2, ok := resp2.externalAddr()
|
||||
if !ok {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
if externalAddr == externalAddr2 {
|
||||
return NATMappingEndpointIndependent
|
||||
}
|
||||
|
||||
// Mapping Test III: Send to other_ip:other_port
|
||||
testIIIAddr := net.UDPAddrFromAddrPort(otherAddr)
|
||||
txID3 := newTransactionID()
|
||||
resp3, _, err := roundTrip(conn, testIIIAddr, txID3, nil, rto)
|
||||
if err != nil {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
externalAddr3, ok := resp3.externalAddr()
|
||||
if !ok {
|
||||
return NATMappingUnknown
|
||||
}
|
||||
|
||||
if externalAddr2 == externalAddr3 {
|
||||
return NATMappingAddressDependent
|
||||
}
|
||||
return NATMappingAddressAndPortDependent
|
||||
}
|
||||
|
||||
func detectNATFiltering(
|
||||
conn net.PacketConn,
|
||||
serverAddr net.Addr,
|
||||
rto time.Duration,
|
||||
) NATFiltering {
|
||||
// Filtering Test II: Request response from different IP and port
|
||||
txID := newTransactionID()
|
||||
_, _, err := roundTrip(conn, serverAddr, txID,
|
||||
[]stunAttribute{changeRequestAttr(changeIP | changePort)}, rto)
|
||||
if err == nil {
|
||||
return NATFilteringEndpointIndependent
|
||||
}
|
||||
|
||||
// Filtering Test III: Request response from different port only
|
||||
txID = newTransactionID()
|
||||
_, _, err = roundTrip(conn, serverAddr, txID,
|
||||
[]stunAttribute{changeRequestAttr(changePort)}, rto)
|
||||
if err == nil {
|
||||
return NATFilteringAddressDependent
|
||||
}
|
||||
|
||||
return NATFilteringAddressAndPortDependent
|
||||
}
|
||||
|
||||
func paddingLen(n int) int {
|
||||
if n%4 == 0 {
|
||||
return 0
|
||||
}
|
||||
return 4 - n%4
|
||||
}
|
||||
@@ -25,6 +25,7 @@ const (
|
||||
TypeTUIC = "tuic"
|
||||
TypeHysteria2 = "hysteria2"
|
||||
TypeTailscale = "tailscale"
|
||||
TypeCloudflared = "cloudflared"
|
||||
TypeDERP = "derp"
|
||||
TypeResolved = "resolved"
|
||||
TypeSSMAPI = "ssm-api"
|
||||
@@ -90,6 +91,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "AnyTLS"
|
||||
case TypeTailscale:
|
||||
return "Tailscale"
|
||||
case TypeCloudflared:
|
||||
return "Cloudflared"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
|
||||
@@ -23,7 +23,8 @@ const (
|
||||
RuleSetVersion2
|
||||
RuleSetVersion3
|
||||
RuleSetVersion4
|
||||
RuleSetVersionCurrent = RuleSetVersion4
|
||||
RuleSetVersion5
|
||||
RuleSetVersionCurrent = RuleSetVersion5
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -6,11 +6,14 @@ import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
"github.com/sagernet/sing-box/common/dialer"
|
||||
"github.com/sagernet/sing-box/common/networkquality"
|
||||
"github.com/sagernet/sing-box/common/stun"
|
||||
"github.com/sagernet/sing-box/common/urltest"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi"
|
||||
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
|
||||
"github.com/sagernet/sing-box/experimental/deprecated"
|
||||
@@ -693,7 +696,7 @@ func (s *StartedService) SetSystemProxyEnabled(ctx context.Context, request *Set
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
return &emptypb.Empty{}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCrashRequest) (*emptypb.Empty, error) {
|
||||
@@ -706,7 +709,7 @@ func (s *StartedService) TriggerDebugCrash(ctx context.Context, request *DebugCr
|
||||
switch request.Type {
|
||||
case DebugCrashRequest_GO:
|
||||
time.AfterFunc(200*time.Millisecond, func() {
|
||||
panic("debug go crash")
|
||||
*(*int)(unsafe.Pointer(uintptr(0))) = 0
|
||||
})
|
||||
case DebugCrashRequest_NATIVE:
|
||||
err := s.handler.TriggerNativeCrash()
|
||||
@@ -1082,31 +1085,6 @@ func (s *StartedService) GetStartedAt(ctx context.Context, empty *emptypb.Empty)
|
||||
return &StartedAt{StartedAt: s.startedAt.UnixMilli()}, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) ListOutbounds(ctx context.Context, _ *emptypb.Empty) (*OutboundList, error) {
|
||||
s.serviceAccess.RLock()
|
||||
if s.serviceStatus.Status != ServiceStatus_STARTED {
|
||||
s.serviceAccess.RUnlock()
|
||||
return nil, os.ErrInvalid
|
||||
}
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
historyStorage := boxService.urlTestHistoryStorage
|
||||
outbounds := boxService.instance.Outbound().Outbounds()
|
||||
var list OutboundList
|
||||
for _, ob := range outbounds {
|
||||
item := &GroupItem{
|
||||
Tag: ob.Tag(),
|
||||
Type: ob.Type(),
|
||||
}
|
||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ob)); history != nil {
|
||||
item.UrlTestTime = history.Time.Unix()
|
||||
item.UrlTestDelay = int32(history.Delay)
|
||||
}
|
||||
list.Outbounds = append(list.Outbounds, item)
|
||||
}
|
||||
return &list, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.ServerStreamingServer[OutboundList]) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
@@ -1126,9 +1104,8 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
historyStorage := boxService.urlTestHistoryStorage
|
||||
outbounds := boxService.instance.Outbound().Outbounds()
|
||||
var list OutboundList
|
||||
for _, ob := range outbounds {
|
||||
for _, ob := range boxService.instance.Outbound().Outbounds() {
|
||||
item := &GroupItem{
|
||||
Tag: ob.Tag(),
|
||||
Type: ob.Type(),
|
||||
@@ -1139,6 +1116,17 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
|
||||
}
|
||||
list.Outbounds = append(list.Outbounds, item)
|
||||
}
|
||||
for _, ep := range boxService.instance.Endpoint().Endpoints() {
|
||||
item := &GroupItem{
|
||||
Tag: ep.Tag(),
|
||||
Type: ep.Type(),
|
||||
}
|
||||
if history := historyStorage.LoadURLTestHistory(adapter.OutboundTag(ep)); history != nil {
|
||||
item.UrlTestTime = history.Time.Unix()
|
||||
item.UrlTestDelay = int32(history.Delay)
|
||||
}
|
||||
list.Outbounds = append(list.Outbounds, item)
|
||||
}
|
||||
err = server.Send(&list)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -1155,6 +1143,17 @@ func (s *StartedService) SubscribeOutbounds(_ *emptypb.Empty, server grpc.Server
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOutbound(instance *Instance, tag string) (adapter.Outbound, error) {
|
||||
if tag == "" {
|
||||
return instance.instance.Outbound().Default(), nil
|
||||
}
|
||||
outbound, loaded := instance.instance.Outbound().Outbound(tag)
|
||||
if !loaded {
|
||||
return nil, E.New("outbound not found: ", tag)
|
||||
}
|
||||
return outbound, nil
|
||||
}
|
||||
|
||||
func (s *StartedService) StartNetworkQualityTest(
|
||||
request *NetworkQualityTestRequest,
|
||||
server grpc.ServerStreamingServer[NetworkQualityTestProgress],
|
||||
@@ -1167,27 +1166,27 @@ func (s *StartedService) StartNetworkQualityTest(
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
var outbound adapter.Outbound
|
||||
if request.OutboundTag == "" {
|
||||
outbound = boxService.instance.Outbound().Default()
|
||||
} else {
|
||||
var loaded bool
|
||||
outbound, loaded = boxService.instance.Outbound().Outbound(request.OutboundTag)
|
||||
if !loaded {
|
||||
return E.New("outbound not found: ", request.OutboundTag)
|
||||
}
|
||||
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
||||
httpClient := networkquality.NewHTTPClient(resolvedDialer)
|
||||
defer httpClient.CloseIdleConnections()
|
||||
|
||||
measurementClientFactory, err := networkquality.NewOptionalHTTP3Factory(resolvedDialer, request.Http3)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
result, nqErr := networkquality.Run(networkquality.Options{
|
||||
ConfigURL: request.ConfigURL,
|
||||
HTTPClient: httpClient,
|
||||
Serial: request.Serial,
|
||||
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
|
||||
Context: server.Context(),
|
||||
ConfigURL: request.ConfigURL,
|
||||
HTTPClient: httpClient,
|
||||
NewMeasurementClient: measurementClientFactory,
|
||||
Serial: request.Serial,
|
||||
MaxRuntime: time.Duration(request.MaxRuntimeSeconds) * time.Second,
|
||||
Context: server.Context(),
|
||||
OnProgress: func(p networkquality.Progress) {
|
||||
_ = server.Send(&NetworkQualityTestProgress{
|
||||
Phase: int32(p.Phase),
|
||||
@@ -1225,6 +1224,247 @@ func (s *StartedService) StartNetworkQualityTest(
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StartedService) StartSTUNTest(
|
||||
request *STUNTestRequest,
|
||||
server grpc.ServerStreamingServer[STUNTestProgress],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
outbound, err := resolveOutbound(boxService, request.OutboundTag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resolvedDialer := dialer.NewResolveDialer(boxService.ctx, outbound, true, "", adapter.DNSQueryOptions{}, 0)
|
||||
|
||||
result, stunErr := stun.Run(stun.Options{
|
||||
Server: request.Server,
|
||||
Dialer: resolvedDialer,
|
||||
Context: server.Context(),
|
||||
OnProgress: func(p stun.Progress) {
|
||||
_ = server.Send(&STUNTestProgress{
|
||||
Phase: int32(p.Phase),
|
||||
ExternalAddr: p.ExternalAddr,
|
||||
LatencyMs: p.LatencyMs,
|
||||
NatMapping: int32(p.NATMapping),
|
||||
NatFiltering: int32(p.NATFiltering),
|
||||
})
|
||||
},
|
||||
})
|
||||
if stunErr != nil {
|
||||
return server.Send(&STUNTestProgress{
|
||||
IsFinal: true,
|
||||
Error: stunErr.Error(),
|
||||
})
|
||||
}
|
||||
return server.Send(&STUNTestProgress{
|
||||
Phase: int32(stun.PhaseDone),
|
||||
ExternalAddr: result.ExternalAddr,
|
||||
LatencyMs: result.LatencyMs,
|
||||
NatMapping: int32(result.NATMapping),
|
||||
NatFiltering: int32(result.NATFiltering),
|
||||
IsFinal: true,
|
||||
NatTypeSupported: result.NATTypeSupported,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StartedService) SubscribeTailscaleStatus(
|
||||
_ *emptypb.Empty,
|
||||
server grpc.ServerStreamingServer[TailscaleStatusUpdate],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
||||
if endpointManager == nil {
|
||||
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
||||
}
|
||||
|
||||
type tailscaleEndpoint struct {
|
||||
tag string
|
||||
provider adapter.TailscaleEndpoint
|
||||
}
|
||||
var endpoints []tailscaleEndpoint
|
||||
for _, endpoint := range endpointManager.Endpoints() {
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
continue
|
||||
}
|
||||
provider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if !loaded {
|
||||
continue
|
||||
}
|
||||
endpoints = append(endpoints, tailscaleEndpoint{
|
||||
tag: endpoint.Tag(),
|
||||
provider: provider,
|
||||
})
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
||||
}
|
||||
|
||||
type taggedStatus struct {
|
||||
tag string
|
||||
status *adapter.TailscaleEndpointStatus
|
||||
}
|
||||
updates := make(chan taggedStatus, len(endpoints))
|
||||
ctx, cancel := context.WithCancel(server.Context())
|
||||
defer cancel()
|
||||
|
||||
var waitGroup sync.WaitGroup
|
||||
for _, endpoint := range endpoints {
|
||||
waitGroup.Add(1)
|
||||
go func(tag string, provider adapter.TailscaleEndpoint) {
|
||||
defer waitGroup.Done()
|
||||
_ = provider.SubscribeTailscaleStatus(ctx, func(endpointStatus *adapter.TailscaleEndpointStatus) {
|
||||
select {
|
||||
case updates <- taggedStatus{tag: tag, status: endpointStatus}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
})
|
||||
}(endpoint.tag, endpoint.provider)
|
||||
}
|
||||
|
||||
go func() {
|
||||
waitGroup.Wait()
|
||||
close(updates)
|
||||
}()
|
||||
|
||||
var tags []string
|
||||
statuses := make(map[string]*adapter.TailscaleEndpointStatus, len(endpoints))
|
||||
for update := range updates {
|
||||
if _, exists := statuses[update.tag]; !exists {
|
||||
tags = append(tags, update.tag)
|
||||
}
|
||||
statuses[update.tag] = update.status
|
||||
protoEndpoints := make([]*TailscaleEndpointStatus, 0, len(statuses))
|
||||
for _, tag := range tags {
|
||||
protoEndpoints = append(protoEndpoints, tailscaleEndpointStatusToProto(tag, statuses[tag]))
|
||||
}
|
||||
sendErr := server.Send(&TailscaleStatusUpdate{
|
||||
Endpoints: protoEndpoints,
|
||||
})
|
||||
if sendErr != nil {
|
||||
return sendErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tailscaleEndpointStatusToProto(tag string, s *adapter.TailscaleEndpointStatus) *TailscaleEndpointStatus {
|
||||
userGroups := make([]*TailscaleUserGroup, len(s.UserGroups))
|
||||
for i, group := range s.UserGroups {
|
||||
peers := make([]*TailscalePeer, len(group.Peers))
|
||||
for j, peer := range group.Peers {
|
||||
peers[j] = tailscalePeerToProto(peer)
|
||||
}
|
||||
userGroups[i] = &TailscaleUserGroup{
|
||||
UserID: group.UserID,
|
||||
LoginName: group.LoginName,
|
||||
DisplayName: group.DisplayName,
|
||||
ProfilePicURL: group.ProfilePicURL,
|
||||
Peers: peers,
|
||||
}
|
||||
}
|
||||
result := &TailscaleEndpointStatus{
|
||||
EndpointTag: tag,
|
||||
BackendState: s.BackendState,
|
||||
AuthURL: s.AuthURL,
|
||||
NetworkName: s.NetworkName,
|
||||
MagicDNSSuffix: s.MagicDNSSuffix,
|
||||
UserGroups: userGroups,
|
||||
}
|
||||
if s.Self != nil {
|
||||
result.Self = tailscalePeerToProto(s.Self)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func tailscalePeerToProto(peer *adapter.TailscalePeer) *TailscalePeer {
|
||||
return &TailscalePeer{
|
||||
HostName: peer.HostName,
|
||||
DnsName: peer.DNSName,
|
||||
Os: peer.OS,
|
||||
TailscaleIPs: peer.TailscaleIPs,
|
||||
Online: peer.Online,
|
||||
ExitNode: peer.ExitNode,
|
||||
ExitNodeOption: peer.ExitNodeOption,
|
||||
Active: peer.Active,
|
||||
RxBytes: peer.RxBytes,
|
||||
TxBytes: peer.TxBytes,
|
||||
KeyExpiry: peer.KeyExpiry,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StartedService) StartTailscalePing(
|
||||
request *TailscalePingRequest,
|
||||
server grpc.ServerStreamingServer[TailscalePingResponse],
|
||||
) error {
|
||||
err := s.waitForStarted(server.Context())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.serviceAccess.RLock()
|
||||
boxService := s.instance
|
||||
s.serviceAccess.RUnlock()
|
||||
|
||||
endpointManager := service.FromContext[adapter.EndpointManager](boxService.ctx)
|
||||
if endpointManager == nil {
|
||||
return status.Error(codes.FailedPrecondition, "endpoint manager not available")
|
||||
}
|
||||
|
||||
var provider adapter.TailscaleEndpoint
|
||||
if request.EndpointTag != "" {
|
||||
endpoint, loaded := endpointManager.Get(request.EndpointTag)
|
||||
if !loaded {
|
||||
return status.Error(codes.NotFound, "endpoint not found: "+request.EndpointTag)
|
||||
}
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
return status.Error(codes.InvalidArgument, "endpoint is not Tailscale: "+request.EndpointTag)
|
||||
}
|
||||
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if !loaded {
|
||||
return status.Error(codes.FailedPrecondition, "endpoint does not support ping")
|
||||
}
|
||||
provider = pingProvider
|
||||
} else {
|
||||
for _, endpoint := range endpointManager.Endpoints() {
|
||||
if endpoint.Type() != C.TypeTailscale {
|
||||
continue
|
||||
}
|
||||
pingProvider, loaded := endpoint.(adapter.TailscaleEndpoint)
|
||||
if loaded {
|
||||
provider = pingProvider
|
||||
break
|
||||
}
|
||||
}
|
||||
if provider == nil {
|
||||
return status.Error(codes.NotFound, "no Tailscale endpoint found")
|
||||
}
|
||||
}
|
||||
|
||||
return provider.StartTailscalePing(server.Context(), request.PeerIP, func(result *adapter.TailscalePingResult) {
|
||||
_ = server.Send(&TailscalePingResponse{
|
||||
LatencyMs: result.LatencyMs,
|
||||
IsDirect: result.IsDirect,
|
||||
Endpoint: result.Endpoint,
|
||||
DerpRegionID: result.DERPRegionID,
|
||||
DerpRegionCode: result.DERPRegionCode,
|
||||
Error: result.Error,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (s *StartedService) mustEmbedUnimplementedStartedServiceServer() {
|
||||
}
|
||||
|
||||
|
||||
@@ -1886,6 +1886,7 @@ type NetworkQualityTestRequest struct {
|
||||
OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"`
|
||||
Serial bool `protobuf:"varint,3,opt,name=serial,proto3" json:"serial,omitempty"`
|
||||
MaxRuntimeSeconds int32 `protobuf:"varint,4,opt,name=maxRuntimeSeconds,proto3" json:"maxRuntimeSeconds,omitempty"`
|
||||
Http3 bool `protobuf:"varint,5,opt,name=http3,proto3" json:"http3,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -1948,6 +1949,13 @@ func (x *NetworkQualityTestRequest) GetMaxRuntimeSeconds() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *NetworkQualityTestRequest) GetHttp3() bool {
|
||||
if x != nil {
|
||||
return x.Http3
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type NetworkQualityTestProgress struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"`
|
||||
@@ -2088,6 +2096,630 @@ func (x *NetworkQualityTestProgress) GetUploadRPMAccuracy() int32 {
|
||||
return 0
|
||||
}
|
||||
|
||||
type STUNTestRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Server string `protobuf:"bytes,1,opt,name=server,proto3" json:"server,omitempty"`
|
||||
OutboundTag string `protobuf:"bytes,2,opt,name=outboundTag,proto3" json:"outboundTag,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *STUNTestRequest) Reset() {
|
||||
*x = STUNTestRequest{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[29]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *STUNTestRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*STUNTestRequest) ProtoMessage() {}
|
||||
|
||||
func (x *STUNTestRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[29]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use STUNTestRequest.ProtoReflect.Descriptor instead.
|
||||
func (*STUNTestRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{29}
|
||||
}
|
||||
|
||||
func (x *STUNTestRequest) GetServer() string {
|
||||
if x != nil {
|
||||
return x.Server
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *STUNTestRequest) GetOutboundTag() string {
|
||||
if x != nil {
|
||||
return x.OutboundTag
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type STUNTestProgress struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Phase int32 `protobuf:"varint,1,opt,name=phase,proto3" json:"phase,omitempty"`
|
||||
ExternalAddr string `protobuf:"bytes,2,opt,name=externalAddr,proto3" json:"externalAddr,omitempty"`
|
||||
LatencyMs int32 `protobuf:"varint,3,opt,name=latencyMs,proto3" json:"latencyMs,omitempty"`
|
||||
NatMapping int32 `protobuf:"varint,4,opt,name=natMapping,proto3" json:"natMapping,omitempty"`
|
||||
NatFiltering int32 `protobuf:"varint,5,opt,name=natFiltering,proto3" json:"natFiltering,omitempty"`
|
||||
IsFinal bool `protobuf:"varint,6,opt,name=isFinal,proto3" json:"isFinal,omitempty"`
|
||||
Error string `protobuf:"bytes,7,opt,name=error,proto3" json:"error,omitempty"`
|
||||
NatTypeSupported bool `protobuf:"varint,8,opt,name=natTypeSupported,proto3" json:"natTypeSupported,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) Reset() {
|
||||
*x = STUNTestProgress{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[30]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*STUNTestProgress) ProtoMessage() {}
|
||||
|
||||
func (x *STUNTestProgress) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[30]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use STUNTestProgress.ProtoReflect.Descriptor instead.
|
||||
func (*STUNTestProgress) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{30}
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetPhase() int32 {
|
||||
if x != nil {
|
||||
return x.Phase
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetExternalAddr() string {
|
||||
if x != nil {
|
||||
return x.ExternalAddr
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetLatencyMs() int32 {
|
||||
if x != nil {
|
||||
return x.LatencyMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetNatMapping() int32 {
|
||||
if x != nil {
|
||||
return x.NatMapping
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetNatFiltering() int32 {
|
||||
if x != nil {
|
||||
return x.NatFiltering
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetIsFinal() bool {
|
||||
if x != nil {
|
||||
return x.IsFinal
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *STUNTestProgress) GetNatTypeSupported() bool {
|
||||
if x != nil {
|
||||
return x.NatTypeSupported
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type TailscaleStatusUpdate struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Endpoints []*TailscaleEndpointStatus `protobuf:"bytes,1,rep,name=endpoints,proto3" json:"endpoints,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TailscaleStatusUpdate) Reset() {
|
||||
*x = TailscaleStatusUpdate{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[31]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TailscaleStatusUpdate) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TailscaleStatusUpdate) ProtoMessage() {}
|
||||
|
||||
func (x *TailscaleStatusUpdate) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[31]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TailscaleStatusUpdate.ProtoReflect.Descriptor instead.
|
||||
func (*TailscaleStatusUpdate) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{31}
|
||||
}
|
||||
|
||||
func (x *TailscaleStatusUpdate) GetEndpoints() []*TailscaleEndpointStatus {
|
||||
if x != nil {
|
||||
return x.Endpoints
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TailscaleEndpointStatus struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"`
|
||||
BackendState string `protobuf:"bytes,2,opt,name=backendState,proto3" json:"backendState,omitempty"`
|
||||
AuthURL string `protobuf:"bytes,3,opt,name=authURL,proto3" json:"authURL,omitempty"`
|
||||
NetworkName string `protobuf:"bytes,4,opt,name=networkName,proto3" json:"networkName,omitempty"`
|
||||
MagicDNSSuffix string `protobuf:"bytes,5,opt,name=magicDNSSuffix,proto3" json:"magicDNSSuffix,omitempty"`
|
||||
Self *TailscalePeer `protobuf:"bytes,6,opt,name=self,proto3" json:"self,omitempty"`
|
||||
UserGroups []*TailscaleUserGroup `protobuf:"bytes,7,rep,name=userGroups,proto3" json:"userGroups,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) Reset() {
|
||||
*x = TailscaleEndpointStatus{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[32]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TailscaleEndpointStatus) ProtoMessage() {}
|
||||
|
||||
func (x *TailscaleEndpointStatus) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[32]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TailscaleEndpointStatus.ProtoReflect.Descriptor instead.
|
||||
func (*TailscaleEndpointStatus) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{32}
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetEndpointTag() string {
|
||||
if x != nil {
|
||||
return x.EndpointTag
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetBackendState() string {
|
||||
if x != nil {
|
||||
return x.BackendState
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetAuthURL() string {
|
||||
if x != nil {
|
||||
return x.AuthURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetNetworkName() string {
|
||||
if x != nil {
|
||||
return x.NetworkName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetMagicDNSSuffix() string {
|
||||
if x != nil {
|
||||
return x.MagicDNSSuffix
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetSelf() *TailscalePeer {
|
||||
if x != nil {
|
||||
return x.Self
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TailscaleEndpointStatus) GetUserGroups() []*TailscaleUserGroup {
|
||||
if x != nil {
|
||||
return x.UserGroups
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TailscaleUserGroup struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
UserID int64 `protobuf:"varint,1,opt,name=userID,proto3" json:"userID,omitempty"`
|
||||
LoginName string `protobuf:"bytes,2,opt,name=loginName,proto3" json:"loginName,omitempty"`
|
||||
DisplayName string `protobuf:"bytes,3,opt,name=displayName,proto3" json:"displayName,omitempty"`
|
||||
ProfilePicURL string `protobuf:"bytes,4,opt,name=profilePicURL,proto3" json:"profilePicURL,omitempty"`
|
||||
Peers []*TailscalePeer `protobuf:"bytes,5,rep,name=peers,proto3" json:"peers,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) Reset() {
|
||||
*x = TailscaleUserGroup{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[33]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TailscaleUserGroup) ProtoMessage() {}
|
||||
|
||||
func (x *TailscaleUserGroup) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[33]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TailscaleUserGroup.ProtoReflect.Descriptor instead.
|
||||
func (*TailscaleUserGroup) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{33}
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) GetUserID() int64 {
|
||||
if x != nil {
|
||||
return x.UserID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) GetLoginName() string {
|
||||
if x != nil {
|
||||
return x.LoginName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) GetDisplayName() string {
|
||||
if x != nil {
|
||||
return x.DisplayName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) GetProfilePicURL() string {
|
||||
if x != nil {
|
||||
return x.ProfilePicURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscaleUserGroup) GetPeers() []*TailscalePeer {
|
||||
if x != nil {
|
||||
return x.Peers
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type TailscalePeer struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
HostName string `protobuf:"bytes,1,opt,name=hostName,proto3" json:"hostName,omitempty"`
|
||||
DnsName string `protobuf:"bytes,2,opt,name=dnsName,proto3" json:"dnsName,omitempty"`
|
||||
Os string `protobuf:"bytes,3,opt,name=os,proto3" json:"os,omitempty"`
|
||||
TailscaleIPs []string `protobuf:"bytes,4,rep,name=tailscaleIPs,proto3" json:"tailscaleIPs,omitempty"`
|
||||
Online bool `protobuf:"varint,5,opt,name=online,proto3" json:"online,omitempty"`
|
||||
ExitNode bool `protobuf:"varint,6,opt,name=exitNode,proto3" json:"exitNode,omitempty"`
|
||||
ExitNodeOption bool `protobuf:"varint,7,opt,name=exitNodeOption,proto3" json:"exitNodeOption,omitempty"`
|
||||
Active bool `protobuf:"varint,8,opt,name=active,proto3" json:"active,omitempty"`
|
||||
RxBytes int64 `protobuf:"varint,9,opt,name=rxBytes,proto3" json:"rxBytes,omitempty"`
|
||||
TxBytes int64 `protobuf:"varint,10,opt,name=txBytes,proto3" json:"txBytes,omitempty"`
|
||||
KeyExpiry int64 `protobuf:"varint,11,opt,name=keyExpiry,proto3" json:"keyExpiry,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) Reset() {
|
||||
*x = TailscalePeer{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[34]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TailscalePeer) ProtoMessage() {}
|
||||
|
||||
func (x *TailscalePeer) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[34]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TailscalePeer.ProtoReflect.Descriptor instead.
|
||||
func (*TailscalePeer) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{34}
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetHostName() string {
|
||||
if x != nil {
|
||||
return x.HostName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetDnsName() string {
|
||||
if x != nil {
|
||||
return x.DnsName
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetOs() string {
|
||||
if x != nil {
|
||||
return x.Os
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetTailscaleIPs() []string {
|
||||
if x != nil {
|
||||
return x.TailscaleIPs
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetOnline() bool {
|
||||
if x != nil {
|
||||
return x.Online
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetExitNode() bool {
|
||||
if x != nil {
|
||||
return x.ExitNode
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetExitNodeOption() bool {
|
||||
if x != nil {
|
||||
return x.ExitNodeOption
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetActive() bool {
|
||||
if x != nil {
|
||||
return x.Active
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetRxBytes() int64 {
|
||||
if x != nil {
|
||||
return x.RxBytes
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetTxBytes() int64 {
|
||||
if x != nil {
|
||||
return x.TxBytes
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TailscalePeer) GetKeyExpiry() int64 {
|
||||
if x != nil {
|
||||
return x.KeyExpiry
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type TailscalePingRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
EndpointTag string `protobuf:"bytes,1,opt,name=endpointTag,proto3" json:"endpointTag,omitempty"`
|
||||
PeerIP string `protobuf:"bytes,2,opt,name=peerIP,proto3" json:"peerIP,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TailscalePingRequest) Reset() {
|
||||
*x = TailscalePingRequest{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[35]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TailscalePingRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TailscalePingRequest) ProtoMessage() {}
|
||||
|
||||
func (x *TailscalePingRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[35]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TailscalePingRequest.ProtoReflect.Descriptor instead.
|
||||
func (*TailscalePingRequest) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{35}
|
||||
}
|
||||
|
||||
func (x *TailscalePingRequest) GetEndpointTag() string {
|
||||
if x != nil {
|
||||
return x.EndpointTag
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscalePingRequest) GetPeerIP() string {
|
||||
if x != nil {
|
||||
return x.PeerIP
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type TailscalePingResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
LatencyMs float64 `protobuf:"fixed64,1,opt,name=latencyMs,proto3" json:"latencyMs,omitempty"`
|
||||
IsDirect bool `protobuf:"varint,2,opt,name=isDirect,proto3" json:"isDirect,omitempty"`
|
||||
Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"`
|
||||
DerpRegionID int32 `protobuf:"varint,4,opt,name=derpRegionID,proto3" json:"derpRegionID,omitempty"`
|
||||
DerpRegionCode string `protobuf:"bytes,5,opt,name=derpRegionCode,proto3" json:"derpRegionCode,omitempty"`
|
||||
Error string `protobuf:"bytes,6,opt,name=error,proto3" json:"error,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) Reset() {
|
||||
*x = TailscalePingResponse{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[36]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*TailscalePingResponse) ProtoMessage() {}
|
||||
|
||||
func (x *TailscalePingResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[36]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use TailscalePingResponse.ProtoReflect.Descriptor instead.
|
||||
func (*TailscalePingResponse) Descriptor() ([]byte, []int) {
|
||||
return file_daemon_started_service_proto_rawDescGZIP(), []int{36}
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) GetLatencyMs() float64 {
|
||||
if x != nil {
|
||||
return x.LatencyMs
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) GetIsDirect() bool {
|
||||
if x != nil {
|
||||
return x.IsDirect
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) GetEndpoint() string {
|
||||
if x != nil {
|
||||
return x.Endpoint
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) GetDerpRegionID() int32 {
|
||||
if x != nil {
|
||||
return x.DerpRegionID
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) GetDerpRegionCode() string {
|
||||
if x != nil {
|
||||
return x.DerpRegionCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *TailscalePingResponse) GetError() string {
|
||||
if x != nil {
|
||||
return x.Error
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type Log_Message struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Level LogLevel `protobuf:"varint,1,opt,name=level,proto3,enum=daemon.LogLevel" json:"level,omitempty"`
|
||||
@@ -2098,7 +2730,7 @@ type Log_Message struct {
|
||||
|
||||
func (x *Log_Message) Reset() {
|
||||
*x = Log_Message{}
|
||||
mi := &file_daemon_started_service_proto_msgTypes[29]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[37]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -2110,7 +2742,7 @@ func (x *Log_Message) String() string {
|
||||
func (*Log_Message) ProtoMessage() {}
|
||||
|
||||
func (x *Log_Message) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_daemon_started_service_proto_msgTypes[29]
|
||||
mi := &file_daemon_started_service_proto_msgTypes[37]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -2277,12 +2909,13 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\tStartedAt\x12\x1c\n" +
|
||||
"\tstartedAt\x18\x01 \x01(\x03R\tstartedAt\"?\n" +
|
||||
"\fOutboundList\x12/\n" +
|
||||
"\toutbounds\x18\x01 \x03(\v2\x11.daemon.GroupItemR\toutbounds\"\xa1\x01\n" +
|
||||
"\toutbounds\x18\x01 \x03(\v2\x11.daemon.GroupItemR\toutbounds\"\xb7\x01\n" +
|
||||
"\x19NetworkQualityTestRequest\x12\x1c\n" +
|
||||
"\tconfigURL\x18\x01 \x01(\tR\tconfigURL\x12 \n" +
|
||||
"\voutboundTag\x18\x02 \x01(\tR\voutboundTag\x12\x16\n" +
|
||||
"\x06serial\x18\x03 \x01(\bR\x06serial\x12,\n" +
|
||||
"\x11maxRuntimeSeconds\x18\x04 \x01(\x05R\x11maxRuntimeSeconds\"\x8e\x04\n" +
|
||||
"\x11maxRuntimeSeconds\x18\x04 \x01(\x05R\x11maxRuntimeSeconds\x12\x14\n" +
|
||||
"\x05http3\x18\x05 \x01(\bR\x05http3\"\x8e\x04\n" +
|
||||
"\x1aNetworkQualityTestProgress\x12\x14\n" +
|
||||
"\x05phase\x18\x01 \x01(\x05R\x05phase\x12*\n" +
|
||||
"\x10downloadCapacity\x18\x02 \x01(\x03R\x10downloadCapacity\x12&\n" +
|
||||
@@ -2297,7 +2930,62 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
" \x01(\x05R\x18downloadCapacityAccuracy\x126\n" +
|
||||
"\x16uploadCapacityAccuracy\x18\v \x01(\x05R\x16uploadCapacityAccuracy\x120\n" +
|
||||
"\x13downloadRPMAccuracy\x18\f \x01(\x05R\x13downloadRPMAccuracy\x12,\n" +
|
||||
"\x11uploadRPMAccuracy\x18\r \x01(\x05R\x11uploadRPMAccuracy*U\n" +
|
||||
"\x11uploadRPMAccuracy\x18\r \x01(\x05R\x11uploadRPMAccuracy\"K\n" +
|
||||
"\x0fSTUNTestRequest\x12\x16\n" +
|
||||
"\x06server\x18\x01 \x01(\tR\x06server\x12 \n" +
|
||||
"\voutboundTag\x18\x02 \x01(\tR\voutboundTag\"\x8a\x02\n" +
|
||||
"\x10STUNTestProgress\x12\x14\n" +
|
||||
"\x05phase\x18\x01 \x01(\x05R\x05phase\x12\"\n" +
|
||||
"\fexternalAddr\x18\x02 \x01(\tR\fexternalAddr\x12\x1c\n" +
|
||||
"\tlatencyMs\x18\x03 \x01(\x05R\tlatencyMs\x12\x1e\n" +
|
||||
"\n" +
|
||||
"natMapping\x18\x04 \x01(\x05R\n" +
|
||||
"natMapping\x12\"\n" +
|
||||
"\fnatFiltering\x18\x05 \x01(\x05R\fnatFiltering\x12\x18\n" +
|
||||
"\aisFinal\x18\x06 \x01(\bR\aisFinal\x12\x14\n" +
|
||||
"\x05error\x18\a \x01(\tR\x05error\x12*\n" +
|
||||
"\x10natTypeSupported\x18\b \x01(\bR\x10natTypeSupported\"V\n" +
|
||||
"\x15TailscaleStatusUpdate\x12=\n" +
|
||||
"\tendpoints\x18\x01 \x03(\v2\x1f.daemon.TailscaleEndpointStatusR\tendpoints\"\xaa\x02\n" +
|
||||
"\x17TailscaleEndpointStatus\x12 \n" +
|
||||
"\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\"\n" +
|
||||
"\fbackendState\x18\x02 \x01(\tR\fbackendState\x12\x18\n" +
|
||||
"\aauthURL\x18\x03 \x01(\tR\aauthURL\x12 \n" +
|
||||
"\vnetworkName\x18\x04 \x01(\tR\vnetworkName\x12&\n" +
|
||||
"\x0emagicDNSSuffix\x18\x05 \x01(\tR\x0emagicDNSSuffix\x12)\n" +
|
||||
"\x04self\x18\x06 \x01(\v2\x15.daemon.TailscalePeerR\x04self\x12:\n" +
|
||||
"\n" +
|
||||
"userGroups\x18\a \x03(\v2\x1a.daemon.TailscaleUserGroupR\n" +
|
||||
"userGroups\"\xbf\x01\n" +
|
||||
"\x12TailscaleUserGroup\x12\x16\n" +
|
||||
"\x06userID\x18\x01 \x01(\x03R\x06userID\x12\x1c\n" +
|
||||
"\tloginName\x18\x02 \x01(\tR\tloginName\x12 \n" +
|
||||
"\vdisplayName\x18\x03 \x01(\tR\vdisplayName\x12$\n" +
|
||||
"\rprofilePicURL\x18\x04 \x01(\tR\rprofilePicURL\x12+\n" +
|
||||
"\x05peers\x18\x05 \x03(\v2\x15.daemon.TailscalePeerR\x05peers\"\xbf\x02\n" +
|
||||
"\rTailscalePeer\x12\x1a\n" +
|
||||
"\bhostName\x18\x01 \x01(\tR\bhostName\x12\x18\n" +
|
||||
"\adnsName\x18\x02 \x01(\tR\adnsName\x12\x0e\n" +
|
||||
"\x02os\x18\x03 \x01(\tR\x02os\x12\"\n" +
|
||||
"\ftailscaleIPs\x18\x04 \x03(\tR\ftailscaleIPs\x12\x16\n" +
|
||||
"\x06online\x18\x05 \x01(\bR\x06online\x12\x1a\n" +
|
||||
"\bexitNode\x18\x06 \x01(\bR\bexitNode\x12&\n" +
|
||||
"\x0eexitNodeOption\x18\a \x01(\bR\x0eexitNodeOption\x12\x16\n" +
|
||||
"\x06active\x18\b \x01(\bR\x06active\x12\x18\n" +
|
||||
"\arxBytes\x18\t \x01(\x03R\arxBytes\x12\x18\n" +
|
||||
"\atxBytes\x18\n" +
|
||||
" \x01(\x03R\atxBytes\x12\x1c\n" +
|
||||
"\tkeyExpiry\x18\v \x01(\x03R\tkeyExpiry\"P\n" +
|
||||
"\x14TailscalePingRequest\x12 \n" +
|
||||
"\vendpointTag\x18\x01 \x01(\tR\vendpointTag\x12\x16\n" +
|
||||
"\x06peerIP\x18\x02 \x01(\tR\x06peerIP\"\xcf\x01\n" +
|
||||
"\x15TailscalePingResponse\x12\x1c\n" +
|
||||
"\tlatencyMs\x18\x01 \x01(\x01R\tlatencyMs\x12\x1a\n" +
|
||||
"\bisDirect\x18\x02 \x01(\bR\bisDirect\x12\x1a\n" +
|
||||
"\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\"\n" +
|
||||
"\fderpRegionID\x18\x04 \x01(\x05R\fderpRegionID\x12&\n" +
|
||||
"\x0ederpRegionCode\x18\x05 \x01(\tR\x0ederpRegionCode\x12\x14\n" +
|
||||
"\x05error\x18\x06 \x01(\tR\x05error*U\n" +
|
||||
"\bLogLevel\x12\t\n" +
|
||||
"\x05PANIC\x10\x00\x12\t\n" +
|
||||
"\x05FATAL\x10\x01\x12\t\n" +
|
||||
@@ -2309,7 +2997,7 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\x13ConnectionEventType\x12\x18\n" +
|
||||
"\x14CONNECTION_EVENT_NEW\x10\x00\x12\x1b\n" +
|
||||
"\x17CONNECTION_EVENT_UPDATE\x10\x01\x12\x1b\n" +
|
||||
"\x17CONNECTION_EVENT_CLOSED\x10\x022\xe4\x0e\n" +
|
||||
"\x17CONNECTION_EVENT_CLOSED\x10\x022\x99\x10\n" +
|
||||
"\x0eStartedService\x12=\n" +
|
||||
"\vStopService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12?\n" +
|
||||
"\rReloadService\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\x12K\n" +
|
||||
@@ -2333,10 +3021,12 @@ const file_daemon_started_service_proto_rawDesc = "" +
|
||||
"\x0fCloseConnection\x12\x1e.daemon.CloseConnectionRequest\x1a\x16.google.protobuf.Empty\"\x00\x12G\n" +
|
||||
"\x13CloseAllConnections\x12\x16.google.protobuf.Empty\x1a\x16.google.protobuf.Empty\"\x00\x12M\n" +
|
||||
"\x15GetDeprecatedWarnings\x12\x16.google.protobuf.Empty\x1a\x1a.daemon.DeprecatedWarnings\"\x00\x12;\n" +
|
||||
"\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12?\n" +
|
||||
"\rListOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x00\x12F\n" +
|
||||
"\fGetStartedAt\x12\x16.google.protobuf.Empty\x1a\x11.daemon.StartedAt\"\x00\x12F\n" +
|
||||
"\x12SubscribeOutbounds\x12\x16.google.protobuf.Empty\x1a\x14.daemon.OutboundList\"\x000\x01\x12d\n" +
|
||||
"\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
|
||||
"\x17StartNetworkQualityTest\x12!.daemon.NetworkQualityTestRequest\x1a\".daemon.NetworkQualityTestProgress\"\x000\x01\x12F\n" +
|
||||
"\rStartSTUNTest\x12\x17.daemon.STUNTestRequest\x1a\x18.daemon.STUNTestProgress\"\x000\x01\x12U\n" +
|
||||
"\x18SubscribeTailscaleStatus\x12\x16.google.protobuf.Empty\x1a\x1d.daemon.TailscaleStatusUpdate\"\x000\x01\x12U\n" +
|
||||
"\x12StartTailscalePing\x12\x1c.daemon.TailscalePingRequest\x1a\x1d.daemon.TailscalePingResponse\"\x000\x01B%Z#github.com/sagernet/sing-box/daemonb\x06proto3"
|
||||
|
||||
var (
|
||||
file_daemon_started_service_proto_rawDescOnce sync.Once
|
||||
@@ -2352,7 +3042,7 @@ func file_daemon_started_service_proto_rawDescGZIP() []byte {
|
||||
|
||||
var (
|
||||
file_daemon_started_service_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
|
||||
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 30)
|
||||
file_daemon_started_service_proto_msgTypes = make([]protoimpl.MessageInfo, 38)
|
||||
file_daemon_started_service_proto_goTypes = []any{
|
||||
(LogLevel)(0), // 0: daemon.LogLevel
|
||||
(ConnectionEventType)(0), // 1: daemon.ConnectionEventType
|
||||
@@ -2387,14 +3077,22 @@ var (
|
||||
(*OutboundList)(nil), // 30: daemon.OutboundList
|
||||
(*NetworkQualityTestRequest)(nil), // 31: daemon.NetworkQualityTestRequest
|
||||
(*NetworkQualityTestProgress)(nil), // 32: daemon.NetworkQualityTestProgress
|
||||
(*Log_Message)(nil), // 33: daemon.Log.Message
|
||||
(*emptypb.Empty)(nil), // 34: google.protobuf.Empty
|
||||
(*STUNTestRequest)(nil), // 33: daemon.STUNTestRequest
|
||||
(*STUNTestProgress)(nil), // 34: daemon.STUNTestProgress
|
||||
(*TailscaleStatusUpdate)(nil), // 35: daemon.TailscaleStatusUpdate
|
||||
(*TailscaleEndpointStatus)(nil), // 36: daemon.TailscaleEndpointStatus
|
||||
(*TailscaleUserGroup)(nil), // 37: daemon.TailscaleUserGroup
|
||||
(*TailscalePeer)(nil), // 38: daemon.TailscalePeer
|
||||
(*TailscalePingRequest)(nil), // 39: daemon.TailscalePingRequest
|
||||
(*TailscalePingResponse)(nil), // 40: daemon.TailscalePingResponse
|
||||
(*Log_Message)(nil), // 41: daemon.Log.Message
|
||||
(*emptypb.Empty)(nil), // 42: google.protobuf.Empty
|
||||
}
|
||||
)
|
||||
|
||||
var file_daemon_started_service_proto_depIdxs = []int32{
|
||||
2, // 0: daemon.ServiceStatus.status:type_name -> daemon.ServiceStatus.Type
|
||||
33, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
|
||||
41, // 1: daemon.Log.messages:type_name -> daemon.Log.Message
|
||||
0, // 2: daemon.DefaultLogLevel.level:type_name -> daemon.LogLevel
|
||||
11, // 3: daemon.Groups.group:type_name -> daemon.Group
|
||||
12, // 4: daemon.Group.items:type_name -> daemon.GroupItem
|
||||
@@ -2405,64 +3103,72 @@ var file_daemon_started_service_proto_depIdxs = []int32{
|
||||
25, // 9: daemon.Connection.processInfo:type_name -> daemon.ProcessInfo
|
||||
28, // 10: daemon.DeprecatedWarnings.warnings:type_name -> daemon.DeprecatedWarning
|
||||
12, // 11: daemon.OutboundList.outbounds:type_name -> daemon.GroupItem
|
||||
0, // 12: daemon.Log.Message.level:type_name -> daemon.LogLevel
|
||||
34, // 13: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
|
||||
34, // 14: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
|
||||
34, // 15: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
|
||||
34, // 16: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
|
||||
34, // 17: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
|
||||
34, // 18: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
|
||||
6, // 19: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
|
||||
34, // 20: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
|
||||
34, // 21: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
|
||||
34, // 22: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
|
||||
16, // 23: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
|
||||
13, // 24: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
|
||||
14, // 25: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
|
||||
15, // 26: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
|
||||
34, // 27: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
|
||||
19, // 28: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
|
||||
20, // 29: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
|
||||
34, // 30: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty
|
||||
21, // 31: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
|
||||
26, // 32: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
|
||||
34, // 33: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
|
||||
34, // 34: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
|
||||
34, // 35: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
|
||||
34, // 36: daemon.StartedService.ListOutbounds:input_type -> google.protobuf.Empty
|
||||
34, // 37: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty
|
||||
31, // 38: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest
|
||||
34, // 39: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
|
||||
34, // 40: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
|
||||
4, // 41: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
|
||||
7, // 42: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
|
||||
8, // 43: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
|
||||
34, // 44: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
|
||||
9, // 45: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
|
||||
10, // 46: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
|
||||
17, // 47: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
|
||||
16, // 48: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
|
||||
34, // 49: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
|
||||
34, // 50: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
|
||||
34, // 51: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
|
||||
34, // 52: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
|
||||
18, // 53: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
|
||||
34, // 54: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
|
||||
34, // 55: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
|
||||
34, // 56: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty
|
||||
23, // 57: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
|
||||
34, // 58: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
|
||||
34, // 59: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
|
||||
27, // 60: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
|
||||
29, // 61: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
|
||||
30, // 62: daemon.StartedService.ListOutbounds:output_type -> daemon.OutboundList
|
||||
30, // 63: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList
|
||||
32, // 64: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
|
||||
39, // [39:65] is the sub-list for method output_type
|
||||
13, // [13:39] is the sub-list for method input_type
|
||||
13, // [13:13] is the sub-list for extension type_name
|
||||
13, // [13:13] is the sub-list for extension extendee
|
||||
0, // [0:13] is the sub-list for field type_name
|
||||
36, // 12: daemon.TailscaleStatusUpdate.endpoints:type_name -> daemon.TailscaleEndpointStatus
|
||||
38, // 13: daemon.TailscaleEndpointStatus.self:type_name -> daemon.TailscalePeer
|
||||
37, // 14: daemon.TailscaleEndpointStatus.userGroups:type_name -> daemon.TailscaleUserGroup
|
||||
38, // 15: daemon.TailscaleUserGroup.peers:type_name -> daemon.TailscalePeer
|
||||
0, // 16: daemon.Log.Message.level:type_name -> daemon.LogLevel
|
||||
42, // 17: daemon.StartedService.StopService:input_type -> google.protobuf.Empty
|
||||
42, // 18: daemon.StartedService.ReloadService:input_type -> google.protobuf.Empty
|
||||
42, // 19: daemon.StartedService.SubscribeServiceStatus:input_type -> google.protobuf.Empty
|
||||
42, // 20: daemon.StartedService.SubscribeLog:input_type -> google.protobuf.Empty
|
||||
42, // 21: daemon.StartedService.GetDefaultLogLevel:input_type -> google.protobuf.Empty
|
||||
42, // 22: daemon.StartedService.ClearLogs:input_type -> google.protobuf.Empty
|
||||
6, // 23: daemon.StartedService.SubscribeStatus:input_type -> daemon.SubscribeStatusRequest
|
||||
42, // 24: daemon.StartedService.SubscribeGroups:input_type -> google.protobuf.Empty
|
||||
42, // 25: daemon.StartedService.GetClashModeStatus:input_type -> google.protobuf.Empty
|
||||
42, // 26: daemon.StartedService.SubscribeClashMode:input_type -> google.protobuf.Empty
|
||||
16, // 27: daemon.StartedService.SetClashMode:input_type -> daemon.ClashMode
|
||||
13, // 28: daemon.StartedService.URLTest:input_type -> daemon.URLTestRequest
|
||||
14, // 29: daemon.StartedService.SelectOutbound:input_type -> daemon.SelectOutboundRequest
|
||||
15, // 30: daemon.StartedService.SetGroupExpand:input_type -> daemon.SetGroupExpandRequest
|
||||
42, // 31: daemon.StartedService.GetSystemProxyStatus:input_type -> google.protobuf.Empty
|
||||
19, // 32: daemon.StartedService.SetSystemProxyEnabled:input_type -> daemon.SetSystemProxyEnabledRequest
|
||||
20, // 33: daemon.StartedService.TriggerDebugCrash:input_type -> daemon.DebugCrashRequest
|
||||
42, // 34: daemon.StartedService.TriggerOOMReport:input_type -> google.protobuf.Empty
|
||||
21, // 35: daemon.StartedService.SubscribeConnections:input_type -> daemon.SubscribeConnectionsRequest
|
||||
26, // 36: daemon.StartedService.CloseConnection:input_type -> daemon.CloseConnectionRequest
|
||||
42, // 37: daemon.StartedService.CloseAllConnections:input_type -> google.protobuf.Empty
|
||||
42, // 38: daemon.StartedService.GetDeprecatedWarnings:input_type -> google.protobuf.Empty
|
||||
42, // 39: daemon.StartedService.GetStartedAt:input_type -> google.protobuf.Empty
|
||||
42, // 40: daemon.StartedService.SubscribeOutbounds:input_type -> google.protobuf.Empty
|
||||
31, // 41: daemon.StartedService.StartNetworkQualityTest:input_type -> daemon.NetworkQualityTestRequest
|
||||
33, // 42: daemon.StartedService.StartSTUNTest:input_type -> daemon.STUNTestRequest
|
||||
42, // 43: daemon.StartedService.SubscribeTailscaleStatus:input_type -> google.protobuf.Empty
|
||||
39, // 44: daemon.StartedService.StartTailscalePing:input_type -> daemon.TailscalePingRequest
|
||||
42, // 45: daemon.StartedService.StopService:output_type -> google.protobuf.Empty
|
||||
42, // 46: daemon.StartedService.ReloadService:output_type -> google.protobuf.Empty
|
||||
4, // 47: daemon.StartedService.SubscribeServiceStatus:output_type -> daemon.ServiceStatus
|
||||
7, // 48: daemon.StartedService.SubscribeLog:output_type -> daemon.Log
|
||||
8, // 49: daemon.StartedService.GetDefaultLogLevel:output_type -> daemon.DefaultLogLevel
|
||||
42, // 50: daemon.StartedService.ClearLogs:output_type -> google.protobuf.Empty
|
||||
9, // 51: daemon.StartedService.SubscribeStatus:output_type -> daemon.Status
|
||||
10, // 52: daemon.StartedService.SubscribeGroups:output_type -> daemon.Groups
|
||||
17, // 53: daemon.StartedService.GetClashModeStatus:output_type -> daemon.ClashModeStatus
|
||||
16, // 54: daemon.StartedService.SubscribeClashMode:output_type -> daemon.ClashMode
|
||||
42, // 55: daemon.StartedService.SetClashMode:output_type -> google.protobuf.Empty
|
||||
42, // 56: daemon.StartedService.URLTest:output_type -> google.protobuf.Empty
|
||||
42, // 57: daemon.StartedService.SelectOutbound:output_type -> google.protobuf.Empty
|
||||
42, // 58: daemon.StartedService.SetGroupExpand:output_type -> google.protobuf.Empty
|
||||
18, // 59: daemon.StartedService.GetSystemProxyStatus:output_type -> daemon.SystemProxyStatus
|
||||
42, // 60: daemon.StartedService.SetSystemProxyEnabled:output_type -> google.protobuf.Empty
|
||||
42, // 61: daemon.StartedService.TriggerDebugCrash:output_type -> google.protobuf.Empty
|
||||
42, // 62: daemon.StartedService.TriggerOOMReport:output_type -> google.protobuf.Empty
|
||||
23, // 63: daemon.StartedService.SubscribeConnections:output_type -> daemon.ConnectionEvents
|
||||
42, // 64: daemon.StartedService.CloseConnection:output_type -> google.protobuf.Empty
|
||||
42, // 65: daemon.StartedService.CloseAllConnections:output_type -> google.protobuf.Empty
|
||||
27, // 66: daemon.StartedService.GetDeprecatedWarnings:output_type -> daemon.DeprecatedWarnings
|
||||
29, // 67: daemon.StartedService.GetStartedAt:output_type -> daemon.StartedAt
|
||||
30, // 68: daemon.StartedService.SubscribeOutbounds:output_type -> daemon.OutboundList
|
||||
32, // 69: daemon.StartedService.StartNetworkQualityTest:output_type -> daemon.NetworkQualityTestProgress
|
||||
34, // 70: daemon.StartedService.StartSTUNTest:output_type -> daemon.STUNTestProgress
|
||||
35, // 71: daemon.StartedService.SubscribeTailscaleStatus:output_type -> daemon.TailscaleStatusUpdate
|
||||
40, // 72: daemon.StartedService.StartTailscalePing:output_type -> daemon.TailscalePingResponse
|
||||
45, // [45:73] is the sub-list for method output_type
|
||||
17, // [17:45] is the sub-list for method input_type
|
||||
17, // [17:17] is the sub-list for extension type_name
|
||||
17, // [17:17] is the sub-list for extension extendee
|
||||
0, // [0:17] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_daemon_started_service_proto_init() }
|
||||
@@ -2476,7 +3182,7 @@ func file_daemon_started_service_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_daemon_started_service_proto_rawDesc), len(file_daemon_started_service_proto_rawDesc)),
|
||||
NumEnums: 4,
|
||||
NumMessages: 30,
|
||||
NumMessages: 38,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
|
||||
@@ -35,9 +35,11 @@ service StartedService {
|
||||
rpc GetDeprecatedWarnings(google.protobuf.Empty) returns(DeprecatedWarnings) {}
|
||||
rpc GetStartedAt(google.protobuf.Empty) returns(StartedAt) {}
|
||||
|
||||
rpc ListOutbounds(google.protobuf.Empty) returns (OutboundList) {}
|
||||
rpc SubscribeOutbounds(google.protobuf.Empty) returns (stream OutboundList) {}
|
||||
rpc StartNetworkQualityTest(NetworkQualityTestRequest) returns (stream NetworkQualityTestProgress) {}
|
||||
rpc StartSTUNTest(STUNTestRequest) returns (stream STUNTestProgress) {}
|
||||
rpc SubscribeTailscaleStatus(google.protobuf.Empty) returns (stream TailscaleStatusUpdate) {}
|
||||
rpc StartTailscalePing(TailscalePingRequest) returns (stream TailscalePingResponse) {}
|
||||
}
|
||||
|
||||
message ServiceStatus {
|
||||
@@ -243,6 +245,7 @@ message NetworkQualityTestRequest {
|
||||
string outboundTag = 2;
|
||||
bool serial = 3;
|
||||
int32 maxRuntimeSeconds = 4;
|
||||
bool http3 = 5;
|
||||
}
|
||||
|
||||
message NetworkQualityTestProgress {
|
||||
@@ -260,3 +263,69 @@ message NetworkQualityTestProgress {
|
||||
int32 downloadRPMAccuracy = 12;
|
||||
int32 uploadRPMAccuracy = 13;
|
||||
}
|
||||
|
||||
message STUNTestRequest {
|
||||
string server = 1;
|
||||
string outboundTag = 2;
|
||||
}
|
||||
|
||||
message STUNTestProgress {
|
||||
int32 phase = 1;
|
||||
string externalAddr = 2;
|
||||
int32 latencyMs = 3;
|
||||
int32 natMapping = 4;
|
||||
int32 natFiltering = 5;
|
||||
bool isFinal = 6;
|
||||
string error = 7;
|
||||
bool natTypeSupported = 8;
|
||||
}
|
||||
|
||||
message TailscaleStatusUpdate {
|
||||
repeated TailscaleEndpointStatus endpoints = 1;
|
||||
}
|
||||
|
||||
message TailscaleEndpointStatus {
|
||||
string endpointTag = 1;
|
||||
string backendState = 2;
|
||||
string authURL = 3;
|
||||
string networkName = 4;
|
||||
string magicDNSSuffix = 5;
|
||||
TailscalePeer self = 6;
|
||||
repeated TailscaleUserGroup userGroups = 7;
|
||||
}
|
||||
|
||||
message TailscaleUserGroup {
|
||||
int64 userID = 1;
|
||||
string loginName = 2;
|
||||
string displayName = 3;
|
||||
string profilePicURL = 4;
|
||||
repeated TailscalePeer peers = 5;
|
||||
}
|
||||
|
||||
message TailscalePeer {
|
||||
string hostName = 1;
|
||||
string dnsName = 2;
|
||||
string os = 3;
|
||||
repeated string tailscaleIPs = 4;
|
||||
bool online = 5;
|
||||
bool exitNode = 6;
|
||||
bool exitNodeOption = 7;
|
||||
bool active = 8;
|
||||
int64 rxBytes = 9;
|
||||
int64 txBytes = 10;
|
||||
int64 keyExpiry = 11;
|
||||
}
|
||||
|
||||
message TailscalePingRequest {
|
||||
string endpointTag = 1;
|
||||
string peerIP = 2;
|
||||
}
|
||||
|
||||
message TailscalePingResponse {
|
||||
double latencyMs = 1;
|
||||
bool isDirect = 2;
|
||||
string endpoint = 3;
|
||||
int32 derpRegionID = 4;
|
||||
string derpRegionCode = 5;
|
||||
string error = 6;
|
||||
}
|
||||
|
||||
@@ -15,32 +15,34 @@ import (
|
||||
const _ = grpc.SupportPackageIsVersion9
|
||||
|
||||
const (
|
||||
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
||||
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
||||
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
||||
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
|
||||
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
|
||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||
StartedService_ListOutbounds_FullMethodName = "/daemon.StartedService/ListOutbounds"
|
||||
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
|
||||
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
|
||||
StartedService_StopService_FullMethodName = "/daemon.StartedService/StopService"
|
||||
StartedService_ReloadService_FullMethodName = "/daemon.StartedService/ReloadService"
|
||||
StartedService_SubscribeServiceStatus_FullMethodName = "/daemon.StartedService/SubscribeServiceStatus"
|
||||
StartedService_SubscribeLog_FullMethodName = "/daemon.StartedService/SubscribeLog"
|
||||
StartedService_GetDefaultLogLevel_FullMethodName = "/daemon.StartedService/GetDefaultLogLevel"
|
||||
StartedService_ClearLogs_FullMethodName = "/daemon.StartedService/ClearLogs"
|
||||
StartedService_SubscribeStatus_FullMethodName = "/daemon.StartedService/SubscribeStatus"
|
||||
StartedService_SubscribeGroups_FullMethodName = "/daemon.StartedService/SubscribeGroups"
|
||||
StartedService_GetClashModeStatus_FullMethodName = "/daemon.StartedService/GetClashModeStatus"
|
||||
StartedService_SubscribeClashMode_FullMethodName = "/daemon.StartedService/SubscribeClashMode"
|
||||
StartedService_SetClashMode_FullMethodName = "/daemon.StartedService/SetClashMode"
|
||||
StartedService_URLTest_FullMethodName = "/daemon.StartedService/URLTest"
|
||||
StartedService_SelectOutbound_FullMethodName = "/daemon.StartedService/SelectOutbound"
|
||||
StartedService_SetGroupExpand_FullMethodName = "/daemon.StartedService/SetGroupExpand"
|
||||
StartedService_GetSystemProxyStatus_FullMethodName = "/daemon.StartedService/GetSystemProxyStatus"
|
||||
StartedService_SetSystemProxyEnabled_FullMethodName = "/daemon.StartedService/SetSystemProxyEnabled"
|
||||
StartedService_TriggerDebugCrash_FullMethodName = "/daemon.StartedService/TriggerDebugCrash"
|
||||
StartedService_TriggerOOMReport_FullMethodName = "/daemon.StartedService/TriggerOOMReport"
|
||||
StartedService_SubscribeConnections_FullMethodName = "/daemon.StartedService/SubscribeConnections"
|
||||
StartedService_CloseConnection_FullMethodName = "/daemon.StartedService/CloseConnection"
|
||||
StartedService_CloseAllConnections_FullMethodName = "/daemon.StartedService/CloseAllConnections"
|
||||
StartedService_GetDeprecatedWarnings_FullMethodName = "/daemon.StartedService/GetDeprecatedWarnings"
|
||||
StartedService_GetStartedAt_FullMethodName = "/daemon.StartedService/GetStartedAt"
|
||||
StartedService_SubscribeOutbounds_FullMethodName = "/daemon.StartedService/SubscribeOutbounds"
|
||||
StartedService_StartNetworkQualityTest_FullMethodName = "/daemon.StartedService/StartNetworkQualityTest"
|
||||
StartedService_StartSTUNTest_FullMethodName = "/daemon.StartedService/StartSTUNTest"
|
||||
StartedService_SubscribeTailscaleStatus_FullMethodName = "/daemon.StartedService/SubscribeTailscaleStatus"
|
||||
StartedService_StartTailscalePing_FullMethodName = "/daemon.StartedService/StartTailscalePing"
|
||||
)
|
||||
|
||||
// StartedServiceClient is the client API for StartedService service.
|
||||
@@ -70,9 +72,11 @@ type StartedServiceClient interface {
|
||||
CloseAllConnections(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*emptypb.Empty, error)
|
||||
GetDeprecatedWarnings(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*DeprecatedWarnings, error)
|
||||
GetStartedAt(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*StartedAt, error)
|
||||
ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error)
|
||||
SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error)
|
||||
StartNetworkQualityTest(ctx context.Context, in *NetworkQualityTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[NetworkQualityTestProgress], error)
|
||||
StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error)
|
||||
SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error)
|
||||
StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error)
|
||||
}
|
||||
|
||||
type startedServiceClient struct {
|
||||
@@ -367,16 +371,6 @@ func (c *startedServiceClient) GetStartedAt(ctx context.Context, in *emptypb.Emp
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) ListOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (*OutboundList, error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
out := new(OutboundList)
|
||||
err := c.cc.Invoke(ctx, StartedService_ListOutbounds_FullMethodName, in, out, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *startedServiceClient) SubscribeOutbounds(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[OutboundList], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[6], StartedService_SubscribeOutbounds_FullMethodName, cOpts...)
|
||||
@@ -415,6 +409,63 @@ func (c *startedServiceClient) StartNetworkQualityTest(ctx context.Context, in *
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartNetworkQualityTestClient = grpc.ServerStreamingClient[NetworkQualityTestProgress]
|
||||
|
||||
func (c *startedServiceClient) StartSTUNTest(ctx context.Context, in *STUNTestRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[STUNTestProgress], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[8], StartedService_StartSTUNTest_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[STUNTestRequest, STUNTestProgress]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartSTUNTestClient = grpc.ServerStreamingClient[STUNTestProgress]
|
||||
|
||||
func (c *startedServiceClient) SubscribeTailscaleStatus(ctx context.Context, in *emptypb.Empty, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscaleStatusUpdate], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[9], StartedService_SubscribeTailscaleStatus_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[emptypb.Empty, TailscaleStatusUpdate]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeTailscaleStatusClient = grpc.ServerStreamingClient[TailscaleStatusUpdate]
|
||||
|
||||
func (c *startedServiceClient) StartTailscalePing(ctx context.Context, in *TailscalePingRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[TailscalePingResponse], error) {
|
||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||
stream, err := c.cc.NewStream(ctx, &StartedService_ServiceDesc.Streams[10], StartedService_StartTailscalePing_FullMethodName, cOpts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
x := &grpc.GenericClientStream[TailscalePingRequest, TailscalePingResponse]{ClientStream: stream}
|
||||
if err := x.ClientStream.SendMsg(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := x.ClientStream.CloseSend(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartTailscalePingClient = grpc.ServerStreamingClient[TailscalePingResponse]
|
||||
|
||||
// StartedServiceServer is the server API for StartedService service.
|
||||
// All implementations must embed UnimplementedStartedServiceServer
|
||||
// for forward compatibility.
|
||||
@@ -442,9 +493,11 @@ type StartedServiceServer interface {
|
||||
CloseAllConnections(context.Context, *emptypb.Empty) (*emptypb.Empty, error)
|
||||
GetDeprecatedWarnings(context.Context, *emptypb.Empty) (*DeprecatedWarnings, error)
|
||||
GetStartedAt(context.Context, *emptypb.Empty) (*StartedAt, error)
|
||||
ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error)
|
||||
SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error
|
||||
StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error
|
||||
StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error
|
||||
SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error
|
||||
StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error
|
||||
mustEmbedUnimplementedStartedServiceServer()
|
||||
}
|
||||
|
||||
@@ -547,10 +600,6 @@ func (UnimplementedStartedServiceServer) GetStartedAt(context.Context, *emptypb.
|
||||
return nil, status.Error(codes.Unimplemented, "method GetStartedAt not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) ListOutbounds(context.Context, *emptypb.Empty) (*OutboundList, error) {
|
||||
return nil, status.Error(codes.Unimplemented, "method ListOutbounds not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc.ServerStreamingServer[OutboundList]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeOutbounds not implemented")
|
||||
}
|
||||
@@ -558,6 +607,18 @@ func (UnimplementedStartedServiceServer) SubscribeOutbounds(*emptypb.Empty, grpc
|
||||
func (UnimplementedStartedServiceServer) StartNetworkQualityTest(*NetworkQualityTestRequest, grpc.ServerStreamingServer[NetworkQualityTestProgress]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartNetworkQualityTest not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StartSTUNTest(*STUNTestRequest, grpc.ServerStreamingServer[STUNTestProgress]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartSTUNTest not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) SubscribeTailscaleStatus(*emptypb.Empty, grpc.ServerStreamingServer[TailscaleStatusUpdate]) error {
|
||||
return status.Error(codes.Unimplemented, "method SubscribeTailscaleStatus not implemented")
|
||||
}
|
||||
|
||||
func (UnimplementedStartedServiceServer) StartTailscalePing(*TailscalePingRequest, grpc.ServerStreamingServer[TailscalePingResponse]) error {
|
||||
return status.Error(codes.Unimplemented, "method StartTailscalePing not implemented")
|
||||
}
|
||||
func (UnimplementedStartedServiceServer) mustEmbedUnimplementedStartedServiceServer() {}
|
||||
func (UnimplementedStartedServiceServer) testEmbeddedByValue() {}
|
||||
|
||||
@@ -951,24 +1012,6 @@ func _StartedService_GetStartedAt_Handler(srv interface{}, ctx context.Context,
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_ListOutbounds_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(emptypb.Empty)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(StartedServiceServer).ListOutbounds(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: StartedService_ListOutbounds_FullMethodName,
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(StartedServiceServer).ListOutbounds(ctx, req.(*emptypb.Empty))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _StartedService_SubscribeOutbounds_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(emptypb.Empty)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
@@ -991,6 +1034,39 @@ func _StartedService_StartNetworkQualityTest_Handler(srv interface{}, stream grp
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartNetworkQualityTestServer = grpc.ServerStreamingServer[NetworkQualityTestProgress]
|
||||
|
||||
func _StartedService_StartSTUNTest_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(STUNTestRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).StartSTUNTest(m, &grpc.GenericServerStream[STUNTestRequest, STUNTestProgress]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartSTUNTestServer = grpc.ServerStreamingServer[STUNTestProgress]
|
||||
|
||||
func _StartedService_SubscribeTailscaleStatus_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(emptypb.Empty)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).SubscribeTailscaleStatus(m, &grpc.GenericServerStream[emptypb.Empty, TailscaleStatusUpdate]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_SubscribeTailscaleStatusServer = grpc.ServerStreamingServer[TailscaleStatusUpdate]
|
||||
|
||||
func _StartedService_StartTailscalePing_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
m := new(TailscalePingRequest)
|
||||
if err := stream.RecvMsg(m); err != nil {
|
||||
return err
|
||||
}
|
||||
return srv.(StartedServiceServer).StartTailscalePing(m, &grpc.GenericServerStream[TailscalePingRequest, TailscalePingResponse]{ServerStream: stream})
|
||||
}
|
||||
|
||||
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
|
||||
type StartedService_StartTailscalePingServer = grpc.ServerStreamingServer[TailscalePingResponse]
|
||||
|
||||
// StartedService_ServiceDesc is the grpc.ServiceDesc for StartedService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@@ -1066,10 +1142,6 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetStartedAt",
|
||||
Handler: _StartedService_GetStartedAt_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "ListOutbounds",
|
||||
Handler: _StartedService_ListOutbounds_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
@@ -1112,6 +1184,21 @@ var StartedService_ServiceDesc = grpc.ServiceDesc{
|
||||
Handler: _StartedService_StartNetworkQualityTest_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "StartSTUNTest",
|
||||
Handler: _StartedService_StartSTUNTest_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "SubscribeTailscaleStatus",
|
||||
Handler: _StartedService_SubscribeTailscaleStatus_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
{
|
||||
StreamName: "StartTailscalePing",
|
||||
Handler: _StartedService_StartTailscalePing_Handler,
|
||||
ServerStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "daemon/started_service.proto",
|
||||
}
|
||||
|
||||
+19
-4
@@ -278,6 +278,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
if timeToLive == 0 {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if timeToLive == 0 || record.Header().Ttl > 0 && record.Header().Ttl < timeToLive {
|
||||
timeToLive = record.Header().Ttl
|
||||
}
|
||||
@@ -289,6 +292,9 @@ func (c *Client) Exchange(ctx context.Context, transport adapter.DNSTransport, m
|
||||
}
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = timeToLive
|
||||
}
|
||||
}
|
||||
@@ -376,21 +382,21 @@ func (c *Client) storeCache(transport adapter.DNSTransport, question dns.Questio
|
||||
}
|
||||
if c.disableExpire {
|
||||
if !c.independentCache {
|
||||
c.cache.Add(question, message)
|
||||
c.cache.Add(question, message.Copy())
|
||||
} else {
|
||||
c.transportCache.Add(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message)
|
||||
}, message.Copy())
|
||||
}
|
||||
} else {
|
||||
if !c.independentCache {
|
||||
c.cache.AddWithLifetime(question, message, time.Second*time.Duration(timeToLive))
|
||||
c.cache.AddWithLifetime(question, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
} else {
|
||||
c.transportCache.AddWithLifetime(transportCacheKey{
|
||||
Question: question,
|
||||
transportTag: transport.Tag(),
|
||||
}, message, time.Second*time.Duration(timeToLive))
|
||||
}, message.Copy(), time.Second*time.Duration(timeToLive))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -481,6 +487,9 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
var originTTL int
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
if originTTL == 0 || record.Header().Ttl > 0 && int(record.Header().Ttl) < originTTL {
|
||||
originTTL = int(record.Header().Ttl)
|
||||
}
|
||||
@@ -495,12 +504,18 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
|
||||
duration := uint32(originTTL - nowTTL)
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = record.Header().Ttl - duration
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, recordList := range [][]dns.RR{response.Answer, response.Ns, response.Extra} {
|
||||
for _, record := range recordList {
|
||||
if record.Header().Rrtype == dns.TypeOPT {
|
||||
continue
|
||||
}
|
||||
record.Header().Ttl = uint32(nowTTL)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,11 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||
RcodeSuccess RcodeError = mDNS.RcodeSuccess
|
||||
RcodeServerFailure RcodeError = mDNS.RcodeServerFailure
|
||||
RcodeFormatError RcodeError = mDNS.RcodeFormatError
|
||||
RcodeNameError RcodeError = mDNS.RcodeNameError
|
||||
RcodeRefused RcodeError = mDNS.RcodeRefused
|
||||
)
|
||||
|
||||
type RcodeError int
|
||||
|
||||
@@ -589,12 +589,13 @@ func (r *Router) Exchange(ctx context.Context, message *mDNS.Msg, options adapte
|
||||
return &responseMessage, nil
|
||||
}
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
if r.closing {
|
||||
r.rulesAccess.RUnlock()
|
||||
return nil, E.New("dns router closed")
|
||||
}
|
||||
rules := r.rules
|
||||
legacyDNSMode := r.legacyDNSMode
|
||||
r.rulesAccess.RUnlock()
|
||||
r.logger.DebugContext(ctx, "exchange ", FormatQuestion(message.Question[0].String()))
|
||||
var (
|
||||
response *mDNS.Msg
|
||||
@@ -701,12 +702,13 @@ done:
|
||||
|
||||
func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQueryOptions) ([]netip.Addr, error) {
|
||||
r.rulesAccess.RLock()
|
||||
defer r.rulesAccess.RUnlock()
|
||||
if r.closing {
|
||||
r.rulesAccess.RUnlock()
|
||||
return nil, E.New("dns router closed")
|
||||
}
|
||||
rules := r.rules
|
||||
legacyDNSMode := r.legacyDNSMode
|
||||
r.rulesAccess.RUnlock()
|
||||
var (
|
||||
responseAddrs []netip.Addr
|
||||
err error
|
||||
@@ -839,10 +841,10 @@ func (r *Router) ResetNetwork() {
|
||||
}
|
||||
|
||||
func defaultRuleNeedsLegacyDNSModeFromAddressFilter(rule option.DefaultDNSRule) bool {
|
||||
if rule.IPAcceptAny || rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
|
||||
if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
|
||||
return true
|
||||
}
|
||||
return !rule.MatchResponse && (len(rule.IPCIDR) > 0 || rule.IPIsPrivate)
|
||||
return !rule.MatchResponse && (rule.IPAcceptAny || len(rule.IPCIDR) > 0 || rule.IPIsPrivate)
|
||||
}
|
||||
|
||||
func hasResponseMatchFields(rule option.DefaultDNSRule) bool {
|
||||
@@ -1047,17 +1049,14 @@ func validateLegacyDNSModeDisabledRuleTree(rule option.DNSRule) (bool, error) {
|
||||
|
||||
func validateLegacyDNSModeDisabledDefaultRule(rule option.DefaultDNSRule) (bool, error) {
|
||||
hasResponseRecords := hasResponseMatchFields(rule)
|
||||
if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate) && !rule.MatchResponse {
|
||||
return false, E.New("Response Match Fields (ip_cidr, ip_is_private, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled")
|
||||
if (hasResponseRecords || len(rule.IPCIDR) > 0 || rule.IPIsPrivate || rule.IPAcceptAny) && !rule.MatchResponse {
|
||||
return false, E.New("Response Match Fields (ip_cidr, ip_is_private, ip_accept_any, response_rcode, response_answer, response_ns, response_extra) require match_response to be enabled")
|
||||
}
|
||||
// Intentionally do not reject rule_set here. A referenced rule set may mix
|
||||
// destination-IP predicates with pre-response predicates such as domain items.
|
||||
// When match_response is false, those destination-IP branches fail closed during
|
||||
// pre-response evaluation instead of consuming DNS response state, while sibling
|
||||
// non-response branches remain matchable.
|
||||
if rule.IPAcceptAny { //nolint:staticcheck
|
||||
return false, E.New(deprecated.OptionIPAcceptAny.MessageWithLink())
|
||||
}
|
||||
if rule.RuleSetIPCIDRAcceptEmpty { //nolint:staticcheck
|
||||
return false, E.New(deprecated.OptionRuleSetIPCIDRAcceptEmpty.MessageWithLink())
|
||||
}
|
||||
|
||||
@@ -4,8 +4,6 @@ package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/sagernet/sing-box/adapter"
|
||||
C "github.com/sagernet/sing-box/constant"
|
||||
@@ -14,7 +12,6 @@ import (
|
||||
"github.com/sagernet/sing-box/log"
|
||||
"github.com/sagernet/sing-box/option"
|
||||
"github.com/sagernet/sing/common"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
"github.com/sagernet/sing/common/logger"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
@@ -35,10 +32,8 @@ type Transport struct {
|
||||
logger logger.ContextLogger
|
||||
hosts *hosts.File
|
||||
dialer N.Dialer
|
||||
preferGo bool
|
||||
fallback bool
|
||||
dhcpTransport dhcpTransport
|
||||
resolver net.Resolver
|
||||
}
|
||||
|
||||
type dhcpTransport interface {
|
||||
@@ -52,14 +47,12 @@ func NewTransport(ctx context.Context, logger log.ContextLogger, tag string, opt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
transportAdapter := dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options)
|
||||
return &Transport{
|
||||
TransportAdapter: transportAdapter,
|
||||
TransportAdapter: dns.NewTransportAdapterWithLocalOptions(C.DNSTypeLocal, tag, options),
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
hosts: hosts.NewFile(hosts.DefaultPath),
|
||||
dialer: transportDialer,
|
||||
preferGo: options.PreferGo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -97,44 +90,3 @@ func (t *Transport) Reset() {
|
||||
t.dhcpTransport.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
||||
if len(addresses) > 0 {
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
if !t.fallback {
|
||||
return t.exchange(ctx, message, question.Name)
|
||||
}
|
||||
if t.dhcpTransport != nil {
|
||||
dhcpTransports := t.dhcpTransport.Fetch()
|
||||
if len(dhcpTransports) > 0 {
|
||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpTransports)
|
||||
}
|
||||
}
|
||||
if t.preferGo {
|
||||
// Assuming the user knows what they are doing, we still execute the query which will fail.
|
||||
return t.exchange(ctx, message, question.Name)
|
||||
}
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
var network string
|
||||
if question.Qtype == mDNS.TypeA {
|
||||
network = "ip4"
|
||||
} else {
|
||||
network = "ip6"
|
||||
}
|
||||
addresses, err := t.resolver.LookupNetIP(ctx, network, question.Name)
|
||||
if err != nil {
|
||||
var dnsError *net.DNSError
|
||||
if errors.As(err, &dnsError) && dnsError.IsNotFound {
|
||||
return nil, dns.RcodeRefused
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return dns.FixedResponse(message.Id, question, addresses, C.DefaultDNSTTL), nil
|
||||
}
|
||||
return nil, E.New("only A and AAAA queries are supported on Apple platforms when using TUN and DHCP unavailable.")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
//go:build darwin
|
||||
|
||||
package local
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
#include <resolv.h>
|
||||
#include <netdb.h>
|
||||
|
||||
static void *cgo_res_init() {
|
||||
res_state state = calloc(1, sizeof(struct __res_state));
|
||||
if (state == NULL) return NULL;
|
||||
if (res_ninit(state) != 0) {
|
||||
free(state);
|
||||
return NULL;
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
static void cgo_res_destroy(void *opaque) {
|
||||
res_state state = (res_state)opaque;
|
||||
res_ndestroy(state);
|
||||
free(state);
|
||||
}
|
||||
|
||||
static int cgo_res_nsearch(void *opaque, const char *dname, int class, int type,
|
||||
unsigned char *answer, int anslen,
|
||||
int timeout_seconds,
|
||||
int *out_h_errno) {
|
||||
res_state state = (res_state)opaque;
|
||||
state->retrans = timeout_seconds;
|
||||
state->retry = 1;
|
||||
int n = res_nsearch(state, dname, class, type, answer, anslen);
|
||||
if (n < 0) {
|
||||
*out_h_errno = state->res_h_errno;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
boxC "github.com/sagernet/sing-box/constant"
|
||||
"github.com/sagernet/sing-box/dns"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
|
||||
mDNS "github.com/miekg/dns"
|
||||
)
|
||||
|
||||
func resolvSearch(name string, class, qtype int, timeoutSeconds int) (*mDNS.Msg, error) {
|
||||
state := C.cgo_res_init()
|
||||
if state == nil {
|
||||
return nil, E.New("res_ninit failed")
|
||||
}
|
||||
defer C.cgo_res_destroy(state)
|
||||
|
||||
cName := C.CString(name)
|
||||
defer C.free(unsafe.Pointer(cName))
|
||||
bufSize := 1232
|
||||
for {
|
||||
answer := make([]byte, bufSize)
|
||||
var hErrno C.int
|
||||
n := C.cgo_res_nsearch(state, cName, C.int(class), C.int(qtype),
|
||||
(*C.uchar)(unsafe.Pointer(&answer[0])), C.int(len(answer)),
|
||||
C.int(timeoutSeconds),
|
||||
&hErrno)
|
||||
if n >= 0 {
|
||||
if int(n) > bufSize {
|
||||
bufSize = int(n)
|
||||
continue
|
||||
}
|
||||
var response mDNS.Msg
|
||||
err := response.Unpack(answer[:int(n)])
|
||||
if err != nil {
|
||||
return nil, E.Cause(err, "unpack res_nsearch response")
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
var response mDNS.Msg
|
||||
_ = response.Unpack(answer[:bufSize])
|
||||
if response.Response {
|
||||
if response.Truncated && bufSize < 65535 {
|
||||
bufSize *= 2
|
||||
if bufSize > 65535 {
|
||||
bufSize = 65535
|
||||
}
|
||||
continue
|
||||
}
|
||||
return &response, nil
|
||||
}
|
||||
switch hErrno {
|
||||
case C.HOST_NOT_FOUND:
|
||||
return nil, dns.RcodeNameError
|
||||
case C.TRY_AGAIN:
|
||||
return nil, dns.RcodeNameError
|
||||
case C.NO_RECOVERY:
|
||||
return nil, dns.RcodeServerFailure
|
||||
case C.NO_DATA:
|
||||
return nil, dns.RcodeSuccess
|
||||
default:
|
||||
return nil, E.New("res_nsearch: unknown error ", int(hErrno), " for ", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Transport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||
question := message.Question[0]
|
||||
if question.Qtype == mDNS.TypeA || question.Qtype == mDNS.TypeAAAA {
|
||||
addresses := t.hosts.Lookup(dns.FqdnToDomain(question.Name))
|
||||
if len(addresses) > 0 {
|
||||
return dns.FixedResponse(message.Id, question, addresses, boxC.DefaultDNSTTL), nil
|
||||
}
|
||||
}
|
||||
if t.fallback && t.dhcpTransport != nil {
|
||||
dhcpServers := t.dhcpTransport.Fetch()
|
||||
if len(dhcpServers) > 0 {
|
||||
return t.dhcpTransport.Exchange0(ctx, message, dhcpServers)
|
||||
}
|
||||
}
|
||||
name := question.Name
|
||||
timeoutSeconds := int(boxC.DNSTimeout / time.Second)
|
||||
if deadline, hasDeadline := ctx.Deadline(); hasDeadline {
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
seconds := int(remaining.Seconds())
|
||||
if seconds < 1 {
|
||||
seconds = 1
|
||||
}
|
||||
timeoutSeconds = seconds
|
||||
}
|
||||
type resolvResult struct {
|
||||
response *mDNS.Msg
|
||||
err error
|
||||
}
|
||||
resultCh := make(chan resolvResult, 1)
|
||||
go func() {
|
||||
response, err := resolvSearch(name, int(question.Qclass), int(question.Qtype), timeoutSeconds)
|
||||
resultCh <- resolvResult{response, err}
|
||||
}()
|
||||
var result resolvResult
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case result = <-resultCh:
|
||||
}
|
||||
if result.err != nil {
|
||||
var rcodeError dns.RcodeError
|
||||
if errors.As(result.err, &rcodeError) {
|
||||
return dns.FixedResponseStatus(message, int(rcodeError)), nil
|
||||
}
|
||||
return nil, result.err
|
||||
}
|
||||
result.response.Id = message.Id
|
||||
return result.response, nil
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !darwin
|
||||
|
||||
package local
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,10 +2,61 @@
|
||||
icon: material/alert-decagram
|
||||
---
|
||||
|
||||
#### 1.14.0-alpha.9
|
||||
#### 1.14.0-alpha.10
|
||||
|
||||
* Add `evaluate` DNS rule action and Response Match Fields **1**
|
||||
* `ip_version` and `query_type` now also take effect on internal DNS lookups **2**
|
||||
* Add `package_name_regex` route, DNS and headless rule item **3**
|
||||
* Add cloudflared inbound **4**
|
||||
* Fixes and improvements
|
||||
|
||||
**1**:
|
||||
|
||||
Response Match Fields
|
||||
([`response_rcode`](/configuration/dns/rule/#response_rcode),
|
||||
[`response_answer`](/configuration/dns/rule/#response_answer),
|
||||
[`response_ns`](/configuration/dns/rule/#response_ns),
|
||||
and [`response_extra`](/configuration/dns/rule/#response_extra))
|
||||
match the evaluated DNS response. They are gated by the new
|
||||
[`match_response`](/configuration/dns/rule/#match_response) field and
|
||||
populated by a preceding
|
||||
[`evaluate`](/configuration/dns/rule_action/#evaluate) DNS rule action;
|
||||
the evaluated response can also be returned directly by a
|
||||
[`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
This deprecates the Legacy Address Filter Fields (`ip_cidr`,
|
||||
`ip_is_private` without `match_response`) in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item; all three will be removed
|
||||
in sing-box 1.16.0.
|
||||
See [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
**2**:
|
||||
|
||||
`ip_version` and `query_type` in DNS rules, together with `query_type` in
|
||||
referenced rule-sets, now take effect on every DNS rule evaluation,
|
||||
including matches from internal domain resolutions that do not target a
|
||||
specific DNS server (for example a `resolve` route rule action without
|
||||
`server` set). In earlier versions they were silently ignored in that
|
||||
path. Combining these fields with any of the legacy DNS fields deprecated
|
||||
in **1** in the same DNS configuration is no longer supported and is
|
||||
rejected at startup.
|
||||
See [Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules).
|
||||
|
||||
**3**:
|
||||
|
||||
See [Route Rule](/configuration/route/rule/#package_name_regex),
|
||||
[DNS Rule](/configuration/dns/rule/#package_name_regex) and
|
||||
[Headless Rule](/configuration/rule-set/headless-rule/#package_name_regex).
|
||||
|
||||
**4**:
|
||||
|
||||
See [Cloudflared](/configuration/inbound/cloudflared/).
|
||||
|
||||
#### 1.13.7
|
||||
|
||||
* Fixes and improvement
|
||||
|
||||
#### 1.13.6
|
||||
|
||||
* Fixes and improvements
|
||||
|
||||
@@ -42,6 +42,7 @@ SFA provides an unprivileged TUN implementation through Android VpnService.
|
||||
| `process_path` | :material-close: | No permission |
|
||||
| `process_path_regex` | :material-close: | No permission |
|
||||
| `package_name` | :material-check: | / |
|
||||
| `package_name_regex` | :material-check: | / |
|
||||
| `user` | :material-close: | Use `package_name` instead |
|
||||
| `user_id` | :material-close: | Use `package_name` instead |
|
||||
| `wifi_ssid` | :material-check: | Fine location permission required |
|
||||
|
||||
@@ -44,6 +44,7 @@ SFI/SFM/SFT provides an unprivileged TUN implementation through NetworkExtension
|
||||
| `process_path` | :material-close: | No permission |
|
||||
| `process_path_regex` | :material-close: | No permission |
|
||||
| `package_name` | :material-close: | / |
|
||||
| `package_name_regex` | :material-close: | / |
|
||||
| `user` | :material-close: | No permission |
|
||||
| `user_id` | :material-close: | No permission |
|
||||
| `wifi_ssid` | :material-alert: | Only supported on iOS |
|
||||
|
||||
@@ -8,11 +8,13 @@ icon: material/alert-decagram
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [match_response](#match_response)
|
||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
||||
:material-delete-clock: [ip_accept_any](#ip_accept_any)
|
||||
:material-plus: [response_rcode](#response_rcode)
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [ip_version](#ip_version)
|
||||
:material-alert: [query_type](#query_type)
|
||||
|
||||
!!! quote "Changes in sing-box 1.13.0"
|
||||
|
||||
@@ -130,6 +132,9 @@ icon: material/alert-decagram
|
||||
"package_name": [
|
||||
"com.termux"
|
||||
],
|
||||
"package_name_regex": [
|
||||
"^com\\.termux.*"
|
||||
],
|
||||
"user": [
|
||||
"sekai"
|
||||
],
|
||||
@@ -178,6 +183,7 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"response_rcode": "",
|
||||
"response_answer": [],
|
||||
"response_ns": [],
|
||||
@@ -191,7 +197,6 @@ icon: material/alert-decagram
|
||||
|
||||
// Deprecated
|
||||
|
||||
"ip_accept_any": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
@@ -240,12 +245,46 @@ Tags of [Inbound](/configuration/inbound/).
|
||||
|
||||
#### ip_version
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
This field now also applies when a DNS rule is matched from an internal
|
||||
domain resolution that does not target a specific DNS server, such as a
|
||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
||||
`server` set. In earlier versions, only DNS queries received from a
|
||||
client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
Setting this field makes the DNS rule incompatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
||||
action and [`match_response`](#match_response).
|
||||
|
||||
4 (A DNS query) or 6 (AAAA DNS query).
|
||||
|
||||
Not limited if empty.
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "Changes in sing-box 1.14.0"
|
||||
|
||||
This field now also applies when a DNS rule is matched from an internal
|
||||
domain resolution that does not target a specific DNS server, such as a
|
||||
[`resolve`](../../route/rule_action/#resolve) route rule action without a
|
||||
`server` set. In earlier versions, only DNS queries received from a
|
||||
client evaluated this field. See
|
||||
[Migration](/migration/#ip_version-and-query_type-behavior-changes-in-dns-rules)
|
||||
for the full list.
|
||||
|
||||
Setting this field makes the DNS rule incompatible in the same DNS
|
||||
configuration with Legacy Address Filter Fields in DNS rules, the Legacy
|
||||
`strategy` DNS rule action option, and the Legacy
|
||||
`rule_set_ip_cidr_accept_empty` DNS rule item. To combine with
|
||||
address-based filtering, use the [`evaluate`](../rule_action/#evaluate)
|
||||
action and [`match_response`](#match_response).
|
||||
|
||||
DNS query type. Values can be integers or type name strings.
|
||||
|
||||
#### network
|
||||
@@ -348,6 +387,12 @@ Match process path using regular expression.
|
||||
|
||||
Match android package name.
|
||||
|
||||
#### package_name_regex
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
Match android package name using regular expression.
|
||||
|
||||
#### user
|
||||
|
||||
!!! quote ""
|
||||
@@ -500,7 +545,13 @@ instead of only matching the original query.
|
||||
The evaluated response can also be returned directly by a later [`respond`](/configuration/dns/rule_action/#respond) action.
|
||||
|
||||
Required for Response Match Fields (`response_rcode`, `response_answer`, `response_ns`, `response_extra`).
|
||||
Also required for `ip_cidr` and `ip_is_private` when used with `evaluate` or Response Match Fields.
|
||||
Also required for `ip_cidr`, `ip_is_private`, and `ip_accept_any` when used with `evaluate` or Response Match Fields.
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
Match when the DNS query response contains at least one address.
|
||||
|
||||
#### invert
|
||||
|
||||
@@ -600,17 +651,6 @@ check [Migration](/migration/#migrate-address-filter-fields-to-response-matching
|
||||
|
||||
Make `ip_cidr` rules in rule-sets accept empty query response.
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "Since sing-box 1.12.0"
|
||||
|
||||
!!! failure "Deprecated in sing-box 1.14.0"
|
||||
|
||||
`ip_accept_any` is deprecated and will be removed in sing-box 1.16.0,
|
||||
check [Migration](/migration/#migrate-address-filter-fields-to-response-matching).
|
||||
|
||||
Match any IP with query response.
|
||||
|
||||
### Response Match Fields
|
||||
|
||||
!!! question "Since sing-box 1.14.0"
|
||||
|
||||
@@ -8,11 +8,13 @@ icon: material/alert-decagram
|
||||
:material-plus: [source_hostname](#source_hostname)
|
||||
:material-plus: [match_response](#match_response)
|
||||
:material-delete-clock: [rule_set_ip_cidr_accept_empty](#rule_set_ip_cidr_accept_empty)
|
||||
:material-delete-clock: [ip_accept_any](#ip_accept_any)
|
||||
:material-plus: [response_rcode](#response_rcode)
|
||||
:material-plus: [response_answer](#response_answer)
|
||||
:material-plus: [response_ns](#response_ns)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [response_extra](#response_extra)
|
||||
:material-plus: [package_name_regex](#package_name_regex)
|
||||
:material-alert: [ip_version](#ip_version)
|
||||
:material-alert: [query_type](#query_type)
|
||||
|
||||
!!! quote "sing-box 1.13.0 中的更改"
|
||||
|
||||
@@ -130,6 +132,9 @@ icon: material/alert-decagram
|
||||
"package_name": [
|
||||
"com.termux"
|
||||
],
|
||||
"package_name_regex": [
|
||||
"^com\\.termux.*"
|
||||
],
|
||||
"user": [
|
||||
"sekai"
|
||||
],
|
||||
@@ -178,6 +183,7 @@ icon: material/alert-decagram
|
||||
"192.168.0.1"
|
||||
],
|
||||
"ip_is_private": false,
|
||||
"ip_accept_any": false,
|
||||
"response_rcode": "",
|
||||
"response_answer": [],
|
||||
"response_ns": [],
|
||||
@@ -191,7 +197,6 @@ icon: material/alert-decagram
|
||||
|
||||
// 已弃用
|
||||
|
||||
"ip_accept_any": false,
|
||||
"rule_set_ip_cidr_accept_empty": false,
|
||||
"rule_set_ipcidr_match_source": false,
|
||||
"geosite": [
|
||||
@@ -240,12 +245,38 @@ icon: material/alert-decagram
|
||||
|
||||
#### ip_version
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
||||
[`match_response`](#match_response)。
|
||||
|
||||
4 (A DNS 查询) 或 6 (AAAA DNS 查询)。
|
||||
|
||||
默认不限制。
|
||||
|
||||
#### query_type
|
||||
|
||||
!!! quote "sing-box 1.14.0 中的更改"
|
||||
|
||||
此字段现在也会在 DNS 规则被未指定具体 DNS 服务器的内部域名解析匹配时生效,
|
||||
例如未设置 `server` 的 [`resolve`](../../route/rule_action/#resolve) 路由规则动作。
|
||||
此前只有来自客户端的 DNS 查询才会评估此字段。完整列表参阅
|
||||
[迁移指南](/zh/migration/#dns-规则中的-ip_version-和-query_type-行为更改)。
|
||||
|
||||
在 DNS 规则中设置此字段后,该 DNS 规则在同一 DNS 配置中不能与
|
||||
旧版地址筛选字段 (DNS 规则)、旧版 DNS 规则动作 `strategy` 选项,
|
||||
或旧版 `rule_set_ip_cidr_accept_empty` DNS 规则项共存。如需与
|
||||
基于地址的筛选组合,请使用 [`evaluate`](../rule_action/#evaluate) 动作和
|
||||
[`match_response`](#match_response)。
|
||||
|
||||
DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
#### network
|
||||
@@ -348,6 +379,12 @@ DNS 查询类型。值可以为整数或者类型名称字符串。
|
||||
|
||||
匹配 Android 应用包名。
|
||||
|
||||
#### package_name_regex
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
使用正则表达式匹配 Android 应用包名。
|
||||
|
||||
#### user
|
||||
|
||||
!!! quote ""
|
||||
@@ -498,7 +535,13 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
该已评估的响应也可以被后续的 [`respond`](/zh/configuration/dns/rule_action/#respond) 动作直接返回。
|
||||
|
||||
响应匹配字段(`response_rcode`、`response_answer`、`response_ns`、`response_extra`)需要此选项。
|
||||
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr` 和 `ip_is_private` 也需要此选项。
|
||||
当与 `evaluate` 或响应匹配字段一起使用时,`ip_cidr`、`ip_is_private` 和 `ip_accept_any` 也需要此选项。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
当 DNS 查询响应包含至少一个地址时匹配。
|
||||
|
||||
#### invert
|
||||
|
||||
@@ -599,17 +642,6 @@ Available values: `wifi`, `cellular`, `ethernet` and `other`.
|
||||
|
||||
使规则集中的 `ip_cidr` 规则接受空查询响应。
|
||||
|
||||
#### ip_accept_any
|
||||
|
||||
!!! question "自 sing-box 1.12.0 起"
|
||||
|
||||
!!! failure "已在 sing-box 1.14.0 废弃"
|
||||
|
||||
`ip_accept_any` 已废弃且将在 sing-box 1.16.0 中被移除,
|
||||
参阅[迁移指南](/zh/migration/#迁移地址筛选字段到响应匹配)。
|
||||
|
||||
匹配任意 IP。
|
||||
|
||||
### 响应匹配字段
|
||||
|
||||
!!! question "自 sing-box 1.14.0 起"
|
||||
|
||||
@@ -44,7 +44,7 @@ Tag of target server.
|
||||
|
||||
`strategy` is deprecated in sing-box 1.14.0 and will be removed in sing-box 1.16.0.
|
||||
|
||||
Set domain strategy for this query. Deprecated, check [Migration](/migration/#migrate-dns-rule-action-strategy-to-rule-items).
|
||||
Set domain strategy for this query.
|
||||
|
||||
One of `prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`.
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ icon: material/new-box
|
||||
|
||||
`strategy` 已在 sing-box 1.14.0 废弃,且将在 sing-box 1.16.0 中被移除。
|
||||
|
||||
为此查询设置域名策略。已废弃,参阅[迁移指南](/zh/migration/#迁移-dns-规则动作-strategy-到规则项)。
|
||||
为此查询设置域名策略。
|
||||
|
||||
可选项:`prefer_ipv4` `prefer_ipv6` `ipv4_only` `ipv6_only`。
|
||||
|
||||
|
||||
@@ -73,24 +73,55 @@ Example:
|
||||
|
||||
=== "Use hosts if available"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "hosts"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
@@ -73,24 +73,55 @@ hosts 文件路径列表。
|
||||
|
||||
=== "如果可用则使用 hosts"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "hosts"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
...
|
||||
},
|
||||
{
|
||||
"type": "hosts",
|
||||
"tag": "hosts"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "hosts"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
@@ -43,29 +43,62 @@ If not enabled, `NXDOMAIN` will be returned for requests that do not match searc
|
||||
|
||||
=== "Split DNS only"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
=== ":material-card-multiple: sing-box 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"server": "resolved"
|
||||
},
|
||||
{
|
||||
"match_response": true,
|
||||
"ip_accept_any": true,
|
||||
"action": "respond"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "resolved"
|
||||
}
|
||||
```
|
||||
|
||||
=== ":material-card-remove: sing-box < 1.14.0"
|
||||
|
||||
```json
|
||||
{
|
||||
"dns": {
|
||||
"servers": [
|
||||
{
|
||||
"type": "local",
|
||||
"tag": "local"
|
||||
},
|
||||
{
|
||||
"type": "resolved",
|
||||
"tag": "resolved",
|
||||
"service": "resolved"
|
||||
}
|
||||
],
|
||||
"rules": [
|
||||
{
|
||||
"ip_accept_any": true,
|
||||
"server": "resolved"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
}
|
||||
```
|
||||
|
||||
=== "Use as global DNS"
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user