Update On Fri Apr 10 21:07:38 CEST 2026

This commit is contained in:
github-action[bot]
2026-04-10 21:07:39 +02:00
parent 2ee33bd358
commit a3b4be57ce
187 changed files with 6402 additions and 1516 deletions
+1
View File
@@ -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
@@ -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")
}
}
+75 -39
View File
@@ -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
}
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+330
View File
@@ -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
}
+10 -1
View File
@@ -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
}
+4 -1
View File
@@ -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
+78 -72
View File
@@ -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",
+6 -6
View File
@@ -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 -2
View File
@@ -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"
}
+1 -1
View File
@@ -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",
+43 -43
View File
@@ -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
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -1,5 +1,5 @@
Name: mita
Version: 3.30.0
Version: 3.30.1
Release: 1%{?dist}
Summary: Mieru proxy server
License: GPLv3+
+8 -8
View File
@@ -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.
+8 -8
View File
@@ -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 软件包的版本。
+8
View File
@@ -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")
}
+8
View File
@@ -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())
+18
View File
@@ -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 {
+18
View File
@@ -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 {
+1 -1
View File
@@ -16,5 +16,5 @@
package version
const (
AppVersion = "3.30.0"
AppVersion = "3.30.1"
)
+75 -39
View File
@@ -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
}
+8 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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=
+330
View File
@@ -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
}
+10 -1
View File
@@ -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
}
+4 -1
View File
@@ -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 -1
View File
@@ -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
@@ -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")
@@ -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() {
+4 -4
View File
@@ -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
+49
View File
@@ -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
View File
@@ -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())
+2 -2
View File
@@ -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)
}
}
+79
View File
@@ -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
}
+4 -1
View File
@@ -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
+4 -1
View File
@@ -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
+36 -3
View File
@@ -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
+55
View File
@@ -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
}
+12
View File
@@ -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")
+607
View File
@@ -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
}
+3
View File
@@ -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:
+2 -1
View File
@@ -23,7 +23,8 @@ const (
RuleSetVersion2
RuleSetVersion3
RuleSetVersion4
RuleSetVersionCurrent = RuleSetVersion4
RuleSetVersion5
RuleSetVersionCurrent = RuleSetVersion5
)
const (
+283 -43
View File
@@ -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() {
}
+778 -72
View File
@@ -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,
},
+70 -1
View File
@@ -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;
}
+151 -64
View File
@@ -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
View File
@@ -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 -4
View File
@@ -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
+8 -9
View File
@@ -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())
}
+1 -49
View File
@@ -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 (
+52 -1
View File
@@ -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 |
+1
View File
@@ -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 |
+55 -15
View File
@@ -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"
+47 -15
View File
@@ -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`
+50 -19
View File
@@ -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