Merge branch 'dev-next' of https://github.com/SagerNet/sing-box into dev-next

 Conflicts:
	adapter/router.go
	dns/client.go
	dns/router.go
	dns/transport/https.go
	dns/transport/local/local.go
	dns/transport/quic/http3.go
	dns/transport/quic/quic.go
	dns/transport/tcp.go
	dns/transport/tls.go
	dns/transport/udp.go
	docs/changelog.md
	docs/configuration/inbound/anytls.md
	docs/configuration/inbound/anytls.zh.md
	experimental/clashapi/server.go
	go.mod
	go.sum
	option/outbound.go
	route/rule_conds.go
This commit is contained in:
mingtianquan 2025-04-28 18:27:27 +08:00
commit ee262385a5
43 changed files with 470 additions and 193 deletions

View File

@ -189,13 +189,14 @@ jobs:
fi
echo "DIR_NAME=${DIR_NAME}" >> "${GITHUB_ENV}"
PKG_VERSION="${{ needs.calculate_version.outputs.version }}"
PKG_VERSION="${PKG_VERSION//-/\~}-1"
PKG_VERSION="${PKG_VERSION//-/\~}"
echo "PKG_VERSION=${PKG_VERSION}" >> "${GITHUB_ENV}"
- name: Package DEB
if: matrix.debian != ''
run: |
set -xeuo pipefail
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
@ -234,6 +235,7 @@ jobs:
run: |-
set -xeuo pipefail
sudo gem install fpm
sudo apt-get update
sudo apt-get install -y libarchive-tools
cp .fpm_systemd .fpm
fpm -t pacman \

View File

@ -120,6 +120,7 @@ jobs:
set -xeuo pipefail
sudo gem install fpm
sudo apt-get install -y debsigs
cp .fpm_systemd .fpm
fpm -t deb \
--name "${NAME}" \
-v "$PKG_VERSION" \
@ -138,6 +139,7 @@ jobs:
run: |-
set -xeuo pipefail
sudo gem install fpm
cp .fpm_systemd .fpm
fpm -t rpm \
--name "${NAME}" \
-v "$PKG_VERSION" \

@ -1 +1 @@
Subproject commit 55f31c29bb68895ce544e0dfbf852b4b3e32b530
Subproject commit 6a15780ce1659a234816f7248cbc09e8ea54a1be

View File

@ -341,7 +341,17 @@ func (d *DefaultDialer) ListenSerialInterfacePacket(ctx context.Context, destina
}
func (d *DefaultDialer) ListenPacketCompat(network, address string) (net.PacketConn, error) {
return d.udpListener.ListenPacket(context.Background(), network, address)
udpListener := d.udpListener
udpListener.Control = control.Append(udpListener.Control, func(network, address string, conn syscall.RawConn) error {
for _, wgControlFn := range WgControlFns {
err := wgControlFn(network, address, conn)
if err != nil {
return err
}
}
return nil
})
return udpListener.ListenPacket(context.Background(), network, address)
}
func trackConn(conn net.Conn, err error) (net.Conn, error) {

View File

@ -31,13 +31,18 @@ func BitTorrent(_ context.Context, metadata *adapter.InboundContext, reader io.R
return os.ErrInvalid
}
const header = "BitTorrent protocol"
var protocol [19]byte
_, err = reader.Read(protocol[:])
var n int
n, err = reader.Read(protocol[:])
if string(protocol[:n]) != header[:n] {
return os.ErrInvalid
}
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
if string(protocol[:]) != "BitTorrent protocol" {
return os.ErrInvalid
if n < 19 {
return ErrNeedMoreData
}
metadata.Protocol = C.ProtocolBitTorrent

View File

@ -32,6 +32,27 @@ func TestSniffBittorrent(t *testing.T) {
}
}
func TestSniffIncompleteBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e74")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotBittorrent(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("13426974546f7272656e75")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.BitTorrent(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffUTP(t *testing.T) {
t.Parallel()

View File

@ -20,22 +20,36 @@ func StreamDomainNameQuery(readCtx context.Context, metadata *adapter.InboundCon
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
if length == 0 {
if length < 12 {
return os.ErrInvalid
}
buffer := buf.NewSize(int(length))
defer buffer.Release()
_, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
var n int
n, err = buffer.ReadFullFrom(reader, buffer.FreeLen())
packet := buffer.Bytes()
if n > 2 && packet[2]&0x80 != 0 { // QR
return os.ErrInvalid
}
if n > 5 && packet[4] == 0 && packet[5] == 0 { // QDCOUNT
return os.ErrInvalid
}
for i := 6; i < 10; i++ {
// ANCOUNT, NSCOUNT
if n > i && packet[i] != 0 {
return os.ErrInvalid
}
}
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
}
return DomainNameQuery(readCtx, metadata, buffer.Bytes())
return DomainNameQuery(readCtx, metadata, packet)
}
func DomainNameQuery(ctx context.Context, metadata *adapter.InboundContext, packet []byte) error {
var msg mDNS.Msg
err := msg.Unpack(packet)
if err != nil {
if err != nil || msg.Response || len(msg.Question) == 0 || len(msg.Answer) > 0 || len(msg.Ns) > 0 {
return err
}
metadata.Protocol = C.ProtocolDNS

View File

@ -1,6 +1,7 @@
package sniff_test
import (
"bytes"
"context"
"encoding/hex"
"testing"
@ -21,3 +22,32 @@ func TestSniffDNS(t *testing.T) {
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000012a06676f6f676c6503636f6d0000010001")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NoError(t, err)
require.Equal(t, C.ProtocolDNS, metadata.Protocol)
}
func TestSniffIncompleteStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000001000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotStreamDNS(t *testing.T) {
t.Parallel()
query, err := hex.DecodeString("001e740701000000000000000000")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.StreamDomainNameQuery(context.TODO(), &metadata, bytes.NewReader(query))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@ -68,7 +68,7 @@ func PeekStream(ctx context.Context, metadata *adapter.InboundContext, conn net.
}
sniffError = E.Errors(sniffError, err)
}
if !errors.Is(err, ErrNeedMoreData) {
if !errors.Is(sniffError, ErrNeedMoreData) {
break
}
}

View File

@ -15,10 +15,11 @@ func SSH(_ context.Context, metadata *adapter.InboundContext, reader io.Reader)
const sshPrefix = "SSH-2.0-"
bReader := bufio.NewReader(reader)
prefix, err := bReader.Peek(len(sshPrefix))
if string(prefix[:]) != sshPrefix[:len(prefix)] {
return os.ErrInvalid
}
if err != nil {
return E.Cause1(ErrNeedMoreData, err)
} else if string(prefix) != sshPrefix {
return os.ErrInvalid
}
fistLine, _, err := bReader.ReadLine()
if err != nil {

View File

@ -24,3 +24,24 @@ func TestSniffSSH(t *testing.T) {
require.Equal(t, C.ProtocolSSH, metadata.Protocol)
require.Equal(t, "dropbear", metadata.Client)
}
func TestSniffIncompleteSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e30")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.ErrorIs(t, err, sniff.ErrNeedMoreData)
}
func TestSniffNotSSH(t *testing.T) {
t.Parallel()
pkt, err := hex.DecodeString("5353482d322e31")
require.NoError(t, err)
var metadata adapter.InboundContext
err = sniff.SSH(context.TODO(), &metadata, bytes.NewReader(pkt))
require.NotEmpty(t, err)
require.NotErrorIs(t, err, sniff.ErrNeedMoreData)
}

View File

@ -483,7 +483,7 @@ func (c *Client) loadResponse(question dns.Question, transport adapter.DNSTransp
}
func MessageToAddresses(response *dns.Msg) ([]netip.Addr, error) {
if response.Rcode != dns.RcodeSuccess && response.Rcode != dns.RcodeNameError {
if response.Rcode != dns.RcodeSuccess {
return nil, RcodeError(response.Rcode)
}
addresses := make([]netip.Addr, 0, len(response.Answer))

View File

@ -323,6 +323,9 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
err error
)
printResult := func() {
if err == nil && len(responseAddrs) == 0 {
err = E.New("empty result")
}
if err != nil {
if errors.Is(err, ErrResponseRejectedCached) {
r.logger.DebugContext(ctx, "response rejected for ", domain, " (cached)")
@ -331,15 +334,15 @@ func (r *Router) Lookup(ctx context.Context, domain string, options adapter.DNSQ
} else {
r.logger.ErrorContext(ctx, E.Cause(err, "lookup failed for ", domain))
}
} else if len(responseAddrs) == 0 {
r.logger.ErrorContext(ctx, "lookup failed for ", domain, ": empty result")
err = RcodeNameError
}
if err != nil {
err = E.Cause(err, "lookup ", domain)
}
}
responseAddrs, cached = r.client.LookupCache(domain, options.Strategy)
if cached {
if len(responseAddrs) == 0 {
return nil, RcodeNameError
return nil, E.New("lookup ", domain, ": empty result (cached)")
}
return responseAddrs, nil
}

View File

@ -96,6 +96,9 @@ func NewHTTPS(ctx context.Context, logger log.ContextLogger, tag string, options
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewHTTPSRaw(
dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTPS, tag, options.RemoteDNSServerOptions),
logger,

View File

@ -3,6 +3,7 @@ package local
import (
"context"
"math/rand"
"net/netip"
"time"
"github.com/sagernet/sing-box/adapter"
@ -90,8 +91,9 @@ func (t *Transport) exchangeParallel(ctx context.Context, systemConfig *dnsConfi
startRacer := func(ctx context.Context, fqdn string) {
response, err := t.tryOneName(ctx, systemConfig, fqdn, message)
if err == nil {
addresses, _ := dns.MessageToAddresses(response)
if len(addresses) == 0 {
var addresses []netip.Addr
addresses, err = dns.MessageToAddresses(response)
if err == nil && len(addresses) == 0 {
err = E.New(fqdn, ": empty result")
}
}

View File

@ -92,6 +92,9 @@ func NewHTTP3(ctx context.Context, logger log.ContextLogger, tag string, options
if serverAddr.Port == 0 {
serverAddr.Port = 443
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &HTTP3Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeHTTP3, tag, options.RemoteDNSServerOptions),
logger: logger,

View File

@ -16,6 +16,7 @@ import (
sQUIC "github.com/sagernet/sing-quic"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/bufio"
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"
@ -58,6 +59,9 @@ func NewQUIC(ctx context.Context, logger log.ContextLogger, tag string, options
if serverAddr.Port == 0 {
serverAddr.Port = 853
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &Transport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeQUIC, tag, options.RemoteDNSServerOptions),
ctx: ctx,

View File

@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
"github.com/sagernet/sing/common/buf"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
N "github.com/sagernet/sing/common/network"
@ -40,6 +41,9 @@ func NewTCP(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 {
serverAddr.Port = 53
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TCPTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTCP, tag, options),
dialer: transportDialer,

View File

@ -57,6 +57,9 @@ func NewTLS(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 {
serverAddr.Port = 853
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return &TLSTransport{
TransportAdapter: dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeTLS, tag, options.RemoteDNSServerOptions),
logger: logger,

View File

@ -13,6 +13,7 @@ import (
"github.com/sagernet/sing-box/log"
"github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common/buf"
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"
@ -47,6 +48,9 @@ func NewUDP(ctx context.Context, logger log.ContextLogger, tag string, options o
if serverAddr.Port == 0 {
serverAddr.Port = 53
}
if !serverAddr.IsValid() {
return nil, E.New("invalid server address: ", serverAddr)
}
return NewUDPRaw(logger, dns.NewTransportAdapterWithRemoteOptions(C.DNSTypeUDP, tag, options), transportDialer, serverAddr), nil
}

View File

@ -2,7 +2,17 @@
icon: material/alert-decagram
---
#### 1.12.0-beta.4
#### 1.12.0-beta.7
* Fixes and improvements
### 1.11.9
* Fixes and improvements
_We are temporarily unable to update sing-box apps on the App Store because the reviewer mistakenly found that we violated the rules (TestFlight users are not affected)._
#### 1.12.0-beta.5
* Fixes and improvements

View File

@ -42,16 +42,18 @@ AnyTLS padding scheme line array.
Default padding scheme:
```
stop=8
0=30-30
1=100-400
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=9-9,500-1000
4=500-1000
5=500-1000
6=500-1000
7=500-1000
```json
[
"stop=8",
"0=30-30",
"1=100-400",
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
"3=9-9,500-1000",
"4=500-1000",
"5=500-1000",
"6=500-1000",
"7=500-1000"
]
```
#### tls

View File

@ -42,16 +42,18 @@ AnyTLS 填充方案行数组。
默认填充方案:
```
stop=8
0=30-30
1=100-400
2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000
3=9-9,500-1000
4=500-1000
5=500-1000
6=500-1000
7=500-1000
```json
[
"stop=8",
"0=30-30",
"1=100-400",
"2=400-500,c,500-1000,c,500-1000,c,500-1000,c,500-1000",
"3=9-9,500-1000",
"4=500-1000",
"5=500-1000",
"6=500-1000",
"7=500-1000"
]
```
#### tls

View File

@ -8,56 +8,57 @@ icon: material/package
=== ":material-debian: Debian / APT"
```bash
sudo mkdir -p /etc/apt/keyrings &&
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc &&
sudo chmod a+r /etc/apt/keyrings/sagernet.asc &&
echo '
Types: deb
URIs: https://deb.sagernet.org/
Suites: *
Components: *
Enabled: yes
Signed-By: /etc/apt/keyrings/sagernet.asc
' | sudo tee /etc/apt/sources.list.d/sagernet.sources &&
sudo apt-get update &&
sudo apt-get install sing-box # or sing-box-beta
```
```bash
sudo mkdir -p /etc/apt/keyrings &&
sudo curl -fsSL https://sing-box.app/gpg.key -o /etc/apt/keyrings/sagernet.asc &&
sudo chmod a+r /etc/apt/keyrings/sagernet.asc &&
echo '
Types: deb
URIs: https://deb.sagernet.org/
Suites: *
Components: *
Enabled: yes
Signed-By: /etc/apt/keyrings/sagernet.asc
' | sudo tee /etc/apt/sources.list.d/sagernet.sources &&
sudo apt-get update &&
sudo apt-get install sing-box # or sing-box-beta
```
=== ":material-redhat: Redhat / DNF 5"
```bash
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo &&
sudo dnf install sing-box # or sing-box-beta
```
```bash
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo &&
sudo dnf install sing-box # or sing-box-beta
```
=== ":material-redhat: Redhat / DNF 4"
```bash
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
sudo dnf -y install dnf-plugins-core &&
sudo dnf install sing-box # or sing-box-beta
```
```bash
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
sudo dnf -y install dnf-plugins-core &&
sudo dnf install sing-box # or sing-box-beta
```
## :material-download-box: Manual Installation
The script download and install the latest package from GitHub releases for deb or rpm based Linux distributions, ArchLinux and OpenWrt.
```shell
curl -fsSL https://sing-box.app/install.sh | sh
```
or latest beta:
```shell
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta
```
or specific version:
```shell
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version>
```
The script download and install the latest package from GitHub releases
for deb or rpm based Linux distributions, ArchLinux and OpenWrt.
```shell
curl -fsSL https://sing-box.app/install.sh | sh
```
or latest beta:
```shell
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta
```
or specific version:
```shell
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version>
```
## :material-book-lock-open: Managed Installation

View File

@ -26,39 +26,38 @@ icon: material/package
=== ":material-redhat: Redhat / DNF 5"
```bash
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo &&
sudo dnf install sing-box # or sing-box-beta
```
```bash
sudo dnf config-manager addrepo --from-repofile=https://sing-box.app/sing-box.repo &&
sudo dnf install sing-box # or sing-box-beta
```
=== ":material-redhat: Redhat / DNF 4"
```bash
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
sudo dnf -y install dnf-plugins-core &&
sudo dnf install sing-box # or sing-box-beta
```
```bash
sudo dnf config-manager --add-repo https://sing-box.app/sing-box.repo &&
sudo dnf -y install dnf-plugins-core &&
sudo dnf install sing-box # or sing-box-beta
```
## :material-download-box: 手动安装
=== ":material-debian: Debian / DEB"
该脚本从 GitHub 发布中下载并安装最新的软件包,适用于基于 deb 或 rpm 的 Linux 发行版、ArchLinux 和 OpenWrt。
```bash
bash <(curl -fsSL https://sing-box.app/deb-install.sh)
```
```shell
curl -fsSL https://sing-box.app/install.sh | sh
```
=== ":material-redhat: Redhat / RPM"
或最新测试版:
```bash
bash <(curl -fsSL https://sing-box.app/rpm-install.sh)
```
(这适用于任何使用 `rpm``systemd` 的发行版。由于 `rpm` 定义依赖关系的方式,如果安装成功,就多半能用。)
```shell
curl -fsSL https://sing-box.app/install.sh | sh -s -- --beta
```
=== ":simple-archlinux: Archlinux / PKG"
或指定版本:
```bash
bash <(curl -fsSL https://sing-box.app/arch-install.sh)
```
```shell
curl -fsSL https://sing-box.app/install.sh | sh -s -- --version <version>
```
## :material-book-lock-open: 托管安装

View File

@ -3,76 +3,92 @@
download_beta=false
download_version=""
for arg in "$@"; do
if [[ "$arg" == "--beta" ]]; then
download_beta=true
elif [[ "$arg" == "--version" ]]; then
download_version=true
elif [[ "$download_version" == 'true' ]]; then
download_version="$arg"
else
echo "Unknown argument: $arg"
echo "Usage: $0 [--beta] [--version <version>]"
exit 1
fi
while [ $# -gt 0 ]; do
case "$1" in
--beta)
download_beta=true
shift
;;
--version)
shift
if [ $# -eq 0 ]; then
echo "Missing argument for --version"
echo "Usage: $0 [--beta] [--version <version>]"
exit 1
fi
download_version="$1"
shift
;;
*)
echo "Unknown argument: $1"
echo "Usage: $0 [--beta] [--version <version>]"
exit 1
;;
esac
done
if [[ $(command -v dpkg) ]]; then
os="linux"
arch=$(dpkg --print-architecture)
package_suffix=".deb"
package_install="dpkg -i"
elif [[ $(command -v dnf) ]]; then
os="linux"
arch=$(uname -m)
package_suffix=".rpm"
package_install="dnf install -y"
elif [[ $(command -v rpm) ]]; then
os="linux"
arch=$(uname -m)
package_suffix=".rpm"
package_install="rpm -i"
elif [[ $(command -v pacman) ]]; then
if command -v pacman >/dev/null 2>&1; then
os="linux"
arch=$(uname -m)
package_suffix=".pkg.tar.zst"
package_install="pacman -U --noconfirm"
elif [[ $(command -v opkg) ]]; then
elif command -v dpkg >/dev/null 2>&1; then
os="linux"
arch=$(dpkg --print-architecture)
package_suffix=".deb"
package_install="dpkg -i"
elif command -v dnf >/dev/null 2>&1; then
os="linux"
arch=$(uname -m)
package_suffix=".rpm"
package_install="dnf install -y"
elif command -v rpm >/dev/null 2>&1; then
os="linux"
arch=$(uname -m)
package_suffix=".rpm"
package_install="rpm -i"
elif command -v opkg >/dev/null 2>&1; then
os="openwrt"
source /etc/os-release
. /etc/os-release
arch="$OPENWRT_ARCH"
package_suffix=".ipk"
package_install="opkg update && opkg install -y"
package_install="opkg update && opkg install"
else
echo "Missing supported package manager."
exit 1
fi
if [[ -z "$download_version" ]]; then
if [[ "$download_beta" != 'true' ]]; then
if [[ -n "$GITHUB_TOKEN" ]]; then
latest_release=$(curl -s --fail-with-body -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases/latest)
if [ -z "$download_version" ]; then
if [ "$download_beta" != "true" ]; then
if [ -n "$GITHUB_TOKEN" ]; then
latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases/latest)
else
latest_release=$(curl -s --fail-with-body https://api.github.com/repos/SagerNet/sing-box/releases/latest)
latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases/latest)
fi
curl_exit_status=$?
if [[ $curl_exit_status -ne 0 ]]; then
echo "$latest_release"
exit $?
if [ $curl_exit_status -ne 0 ]; then
exit $curl_exit_status
fi
download_version=$(echo "$latest_release" | grep tag_name | cut -d ":" -f2 | sed 's/\"//g;s/\,//g;s/\ //g;s/v//')
if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then
echo "$latest_release"
exit 1
fi
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g')
else
if [[ -n "$GITHUB_TOKEN" ]]; then
latest_release=$(curl -s --fail-with-body -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases)
if [ -n "$GITHUB_TOKEN" ]; then
latest_release=$(curl -s -H "Authorization: token ${GITHUB_TOKEN}" https://api.github.com/repos/SagerNet/sing-box/releases)
else
latest_release=$(curl -s --fail-with-body https://api.github.com/repos/SagerNet/sing-box/releases)
latest_release=$(curl -s https://api.github.com/repos/SagerNet/sing-box/releases)
fi
curl_exit_status=$?
if [[ $? -ne 0 ]]; then
echo "$latest_release"
exit $?
if [ $curl_exit_status -ne 0 ]; then
exit $curl_exit_status
fi
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | cut -d ":" -f2 | sed 's/\"//g;s/\,//g;s/\ //g;s/v//')
if [ "$(echo "$latest_release" | grep tag_name | wc -l)" -eq 0 ]; then
echo "$latest_release"
exit 1
fi
download_version=$(echo "$latest_release" | grep tag_name | head -n 1 | awk -F: '{print $2}' | sed 's/[", v]//g')
fi
fi
@ -80,18 +96,21 @@ package_name="sing-box_${download_version}_${os}_${arch}${package_suffix}"
package_url="https://github.com/SagerNet/sing-box/releases/download/v${download_version}/${package_name}"
echo "Downloading $package_url"
if [[ -n "$GITHUB_TOKEN" ]]; then
curl --fail-with-body -Lo "$package_name" -H "Authorization: token ${GITHUB_TOKEN}" "$package_url"
if [ -n "$GITHUB_TOKEN" ]; then
curl --fail -Lo "$package_name" -H "Authorization: token ${GITHUB_TOKEN}" "$package_url"
else
curl --fail-with-body -Lo "$package_name" "$package_url"
curl --fail -Lo "$package_name" "$package_url"
fi
if [[ $? -ne 0 ]]; then
exit $?
curl_exit_status=$?
if [ $curl_exit_status -ne 0 ]; then
exit $curl_exit_status
fi
if [[ $(command -v sudo) ]]; then
if command -v sudo >/dev/null 2>&1; then
package_install="sudo $package_install"
fi
echo "$package_install $package_name" && $package_install "$package_name" && rm "$package_name"
echo "$package_install $package_name"
sh -c "$package_install \"$package_name\""
rm -f "$package_name"

View File

@ -4,6 +4,7 @@ import (
"bytes"
"net"
"net/http"
"runtime/debug"
"time"
"github.com/sagernet/sing-box/experimental/clashapi/trafficontrol"
@ -12,14 +13,23 @@ import (
"github.com/sagernet/ws/wsutil"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/render"
)
// API created by Clash.Meta
func (s *Server) setupMetaAPI(r chi.Router) {
if s.logDebug {
r := chi.NewRouter()
r.Put("/gc", func(w http.ResponseWriter, r *http.Request) {
debug.FreeOSMemory()
})
r.Mount("/", middleware.Profiler())
}
r.Get("/memory", memory(s.trafficManager))
r.Mount("/group", groupRouter(s))
r.Mount("/upgrade", upgradeRouter(s))
}
type Memory struct {

View File

@ -0,0 +1,36 @@
package clashapi
import (
"net/http"
E "github.com/sagernet/sing/common/exceptions"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
func upgradeRouter(server *Server) http.Handler {
r := chi.NewRouter()
r.Post("/ui", updateExternalUI(server))
return r
}
func updateExternalUI(server *Server) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if server.externalUI == "" {
render.Status(r, http.StatusNotFound)
render.JSON(w, r, newError("external UI not enabled"))
return
}
server.logger.Info("upgrading external UI")
err := server.downloadExternalUI()
if err != nil {
server.logger.Error(E.Cause(err, "upgrade external ui"))
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, newError(err.Error()))
return
}
server.logger.Info("updated external UI")
render.JSON(w, r, render.M{"status": "ok"})
}
}

View File

@ -49,6 +49,8 @@ type Server struct {
httpServer *http.Server
trafficManager *trafficontrol.Manager
urlTestHistory adapter.URLTestHistoryStorage
logDebug bool
mode string
modeList []string
modeUpdateHook chan<- struct{}
@ -74,6 +76,7 @@ func NewServer(ctx context.Context, logFactory log.ObservableFactory, options op
Handler: chiRouter,
},
trafficManager: trafficManager,
logDebug: logFactory.Level() >= log.LevelDebug,
modeList: options.ModeList,
externalController: options.ExternalController != "",
externalUIDownloadURL: options.ExternalUIDownloadURL,

10
go.mod
View File

@ -26,18 +26,18 @@ require (
github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb
github.com/sagernet/quic-go v0.49.0-beta.1
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c
github.com/sagernet/sing v0.6.8-0.20250425035333-84184da91a3a
github.com/sagernet/sing-mux v0.3.1
github.com/sagernet/sing-quic v0.4.1
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76
github.com/sagernet/sing-shadowsocks v0.2.7
github.com/sagernet/sing-shadowsocks2 v0.2.0
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a
github.com/sagernet/sing-vmess v0.2.0
github.com/sagernet/sing-vmess v0.2.1
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7
github.com/sagernet/tailscale v1.80.3-mod.2.0.20250422104142-2421001a70ff
github.com/sagernet/tailscale v1.80.3-mod.4
github.com/sagernet/utls v1.6.7
github.com/sagernet/wireguard-go v0.0.1-beta.5
github.com/sagernet/wireguard-go v0.0.1-beta.7
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.10.0

20
go.sum
View File

@ -178,12 +178,12 @@ github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8W
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c h1:Zi+WR7f9SQ96yNHmyxj42BtaVb3kTouQ8bQLBHReTSI=
github.com/sagernet/sing v0.6.7-0.20250409030945-77e2a1bb577c/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing v0.6.8-0.20250425035333-84184da91a3a h1:oE67hmp5rzLlE6clE7FpK4Hg6yLXsa1Zu3A01vcazb0=
github.com/sagernet/sing v0.6.8-0.20250425035333-84184da91a3a/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
github.com/sagernet/sing-quic v0.4.1 h1:pxlMa4efZu/M07RgGagNNDDyl6ZUwpmNUjRTpgHOWK4=
github.com/sagernet/sing-quic v0.4.1/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76 h1:iwpCX6H3nZEOGUGwx0q5azcgYOA9f6v9YssihXoRKHk=
github.com/sagernet/sing-quic v0.4.1-0.20250423030647-0eb05f373a76/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
@ -192,16 +192,16 @@ github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056 h1:GFNJQ
github.com/sagernet/sing-shadowtls v0.2.1-0.20250316154757-6f9e732e5056/go.mod h1:HyacBPIFiKihJQR8LQp56FM4hBtd/7MZXnRxxQIOPsc=
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a h1:2aLxZFD2HPCLrnFGpH+KBuPqMOk0cuaDE2dgEvANuMk=
github.com/sagernet/sing-tun v0.6.5-0.20250412112220-15069fc1c20a/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
github.com/sagernet/sing-vmess v0.2.1 h1:6izHC2+B68aQCxTagki6eZZc+g5eh4dYwxOV5a2Lhug=
github.com/sagernet/sing-vmess v0.2.1/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/tailscale v1.80.3-mod.2.0.20250422104142-2421001a70ff h1:PK4zmYqcMpW1a416jKR/y7XTqeyANTcbvux8cAA0Vw8=
github.com/sagernet/tailscale v1.80.3-mod.2.0.20250422104142-2421001a70ff/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/tailscale v1.80.3-mod.4 h1:9UgYq8m9mwX5dbTbueVxbRh+bq7AayxemJGM2PkJQnE=
github.com/sagernet/tailscale v1.80.3-mod.4/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=

View File

@ -83,7 +83,6 @@ type DialerOptions struct {
NetworkType badoption.Listable[InterfaceType] `json:"network_type,omitempty"`
FallbackNetworkType badoption.Listable[InterfaceType] `json:"fallback_network_type,omitempty"`
FallbackDelay badoption.Duration `json:"fallback_delay,omitempty"`
IsWireGuardListener bool `json:"-"`
// Deprecated: migrated to domain resolver
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`

View File

@ -10,6 +10,7 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/outbound"
@ -191,9 +192,29 @@ func (s *Outbound) DialContext(ctx context.Context, network string, destination
if err != nil {
return nil, err
}
return client.Dial(network, destination.String())
conn, err := client.Dial(network, destination.String())
if err != nil {
return nil, err
}
return &chanConnWrapper{Conn: conn}, nil
}
func (s *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
return nil, os.ErrInvalid
}
type chanConnWrapper struct {
net.Conn
}
func (c *chanConnWrapper) SetDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *chanConnWrapper) SetReadDeadline(t time.Time) error {
return os.ErrInvalid
}
func (c *chanConnWrapper) SetWriteDeadline(t time.Time) error {
return os.ErrInvalid
}

View File

@ -45,8 +45,8 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
logger: logger,
localAddresses: options.Address,
}
if options.Detour == "" {
options.IsWireGuardListener = true
if options.Detour != "" && options.ListenPort != 0 {
return nil, E.New("`listen_port` is conflict with `detour`")
}
outboundDialer, err := dialer.NewWithOptions(dialer.Options{
Context: ctx,

View File

@ -46,9 +46,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
logger: logger,
localAddresses: options.LocalAddress,
}
if options.Detour == "" {
options.IsWireGuardListener = true
} else if options.GSO {
if options.Detour != "" && options.GSO {
return nil, E.New("gso is conflict with detour")
}
outboundDialer, err := dialer.NewWithOptions(dialer.Options{

View File

@ -7,6 +7,7 @@ import (
"net"
"net/netip"
"os"
"strings"
"sync"
"sync/atomic"
"time"
@ -66,7 +67,17 @@ func (m *ConnectionManager) NewConnection(ctx context.Context, this N.Dialer, co
remoteConn, err = this.DialContext(ctx, N.NetworkTCP, metadata.Destination)
}
if err != nil {
err = E.Cause(err, "open outbound connection")
var remoteString string
if len(metadata.DestinationAddresses) > 0 {
remoteString = "[" + strings.Join(common.Map(metadata.DestinationAddresses, netip.Addr.String), ",") + "]"
} else {
remoteString = metadata.Destination.String()
}
var dialerString string
if outbound, isOutbound := this.(adapter.Outbound); isOutbound {
dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
}
err = E.Cause(err, "open connection to ", remoteString, dialerString)
N.CloseOnHandshakeFailure(conn, onClose, err)
m.logger.ErrorContext(ctx, err)
return
@ -133,8 +144,19 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial
remoteConn, err = this.DialContext(ctx, N.NetworkUDP, metadata.Destination)
}
if err != nil {
var remoteString string
if len(metadata.DestinationAddresses) > 0 {
remoteString = "[" + strings.Join(common.Map(metadata.DestinationAddresses, netip.Addr.String), ",") + "]"
} else {
remoteString = metadata.Destination.String()
}
var dialerString string
if outbound, isOutbound := this.(adapter.Outbound); isOutbound {
dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
}
err = E.Cause(err, "open packet connection to ", remoteString, dialerString)
N.CloseOnHandshakeFailure(conn, onClose, err)
m.logger.ErrorContext(ctx, "open outbound packet connection: ", err)
m.logger.ErrorContext(ctx, err)
return
}
remotePacketConn = bufio.NewUnbindPacketConn(remoteConn)
@ -149,8 +171,13 @@ func (m *ConnectionManager) NewPacketConnection(ctx context.Context, this N.Dial
remotePacketConn, err = this.ListenPacket(ctx, metadata.Destination)
}
if err != nil {
var dialerString string
if outbound, isOutbound := this.(adapter.Outbound); isOutbound {
dialerString = " using outbound/" + outbound.Type() + "[" + outbound.Tag() + "]"
}
err = E.Cause(err, "listen packet connection using ", dialerString)
N.CloseOnHandshakeFailure(conn, onClose, err)
m.logger.ErrorContext(ctx, "listen outbound packet connection: ", err)
m.logger.ErrorContext(ctx, err)
return
}
}

View File

@ -418,6 +418,7 @@ match:
Port: metadata.Destination.Port,
Fqdn: routeOptions.OverrideAddress.Fqdn,
}
metadata.DestinationAddresses = nil
}
if routeOptions.OverridePort > 0 {
metadata.Destination = M.Socksaddr{

View File

@ -102,7 +102,10 @@ func NewDefaultRule(ctx context.Context, logger log.ContextLogger, options optio
rule.allItems = append(rule.allItems, item)
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
item := NewDomainItem(options.Domain, options.DomainSuffix)
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
if err != nil {
return nil, err
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}

View File

@ -93,7 +93,10 @@ func NewDefaultDNSRule(ctx context.Context, logger log.ContextLogger, options op
rule.allItems = append(rule.allItems, item)
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
item := NewDomainItem(options.Domain, options.DomainSuffix)
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
if err != nil {
return nil, err
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
}

View File

@ -47,7 +47,10 @@ func NewDefaultHeadlessRule(ctx context.Context, options option.DefaultHeadlessR
rule.allItems = append(rule.allItems, item)
}
if len(options.Domain) > 0 || len(options.DomainSuffix) > 0 {
item := NewDomainItem(options.Domain, options.DomainSuffix)
item, err := NewDomainItem(options.Domain, options.DomainSuffix)
if err != nil {
return nil, err
}
rule.destinationAddressItems = append(rule.destinationAddressItems, item)
rule.allItems = append(rule.allItems, item)
} else if options.DomainMatcher != nil {

View File

@ -5,6 +5,7 @@ import (
"github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing/common/domain"
E "github.com/sagernet/sing/common/exceptions"
)
var _ RuleItem = (*DomainItem)(nil)
@ -14,7 +15,17 @@ type DomainItem struct {
description string
}
func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem {
func NewDomainItem(domains []string, domainSuffixes []string) (*DomainItem, error) {
for _, domainItem := range domains {
if domainItem == "" {
return nil, E.New("domain: empty item is not allowed")
}
}
for _, domainSuffixItem := range domainSuffixes {
if domainSuffixItem == "" {
return nil, E.New("domain_suffix: empty item is not allowed")
}
}
var description string
if dLen := len(domains); dLen > 0 {
if dLen == 1 {
@ -40,7 +51,7 @@ func NewDomainItem(domains []string, domainSuffixes []string) *DomainItem {
return &DomainItem{
domain.NewMatcher(domains, domainSuffixes, false),
description,
}
}, nil
}
func NewRawDomainItem(matcher *domain.Matcher) *DomainItem {

View File

@ -91,10 +91,7 @@ func (c *Client) dialContext(ctx context.Context, requestURL *url.URL, headers h
} else {
deadlineConn = conn
}
err = deadlineConn.SetDeadline(time.Now().Add(C.TCPTimeout))
if err != nil {
return nil, E.Cause(err, "set read deadline")
}
deadlineConn.SetDeadline(time.Now().Add(C.TCPTimeout))
var protocols []string
if protocolHeader := headers.Get("Sec-WebSocket-Protocol"); protocolHeader != "" {
protocols = []string{protocolHeader}

View File

@ -141,7 +141,7 @@ func (e *Endpoint) Start(resolve bool) error {
return nil
}
var bind conn.Bind
wgListener, isWgListener := e.options.Dialer.(conn.Listener)
wgListener, isWgListener := common.Cast[conn.Listener](e.options.Dialer)
if isWgListener {
bind = conn.NewStdNetBind(wgListener)
} else {