Compare commits

...

11 Commits

Author SHA1 Message Date
Shtorm
a79e16c03c Merge pull request #118 from embogomolov/fix/interrupt-group-deadlock-extended
common/interrupt: close connections outside group lock
2026-07-22 14:22:08 +03:00
embogomolov
d2805ffcc9 common/interrupt: close connections outside group lock 2026-07-22 14:09:56 +03:00
Shtorm
7c19b660b4 Merge pull request #116 from embogomolov/fix/xhttp-h3-udp-socket-leak
transport/v2rayxhttp: close UDP connection when HTTP/3 dial fails
2026-07-22 13:35:15 +03:00
embogomolov
33907d2e23 transport/v2rayxhttp: close UDP connection when HTTP/3 dial fails 2026-07-22 11:41:41 +03:00
Shtorm
ea48501b1d Merge branch 'extended' of https://github.com/shtorm-7/sing-box-extended into extended 2026-07-18 16:40:41 +03:00
Shtorm
ff11f007ec Enable naive proxy build in Docker image 2026-07-18 16:40:15 +03:00
Shtorm
dc4170ad6b Fix GroupCommonOption 2026-07-18 16:40:04 +03:00
Shtorm
03f2be2ee6 Fix MTProxy 2026-07-18 14:21:25 +03:00
Shtorm
6941854ce2 Fix XHTTP 2026-07-18 14:21:06 +03:00
Shtorm
09d2dca3c3 Fix TrustTunnel 2026-07-18 14:20:38 +03:00
Shtorm
425bfaf779 Update Telegram badge to include logo
Signed-off-by: Shtorm <108103062+shtorm-7@users.noreply.github.com>
2026-07-17 16:58:08 +03:00
12 changed files with 133 additions and 57 deletions

View File

@@ -1,18 +1,23 @@
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS builder FROM --platform=$BUILDPLATFORM golang:1.26 AS builder
LABEL maintainer="shtorm-7" LABEL maintainer="shtorm-7"
COPY . /go/src/github.com/sagernet/sing-box COPY . /go/src/github.com/sagernet/sing-box
WORKDIR /go/src/github.com/sagernet/sing-box WORKDIR /go/src/github.com/sagernet/sing-box
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
ARG GOPROXY="" ARG GOPROXY=""
ENV GOPROXY ${GOPROXY} ARG CRONET_GO_PATH=/go/src/github.com/sagernet/sing-box/cronet-go
ENV CGO_ENABLED=0 ENV GOPROXY=${GOPROXY}
ENV CGO_ENABLED=1
ENV GOOS=$TARGETOS ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH ENV GOARCH=$TARGETARCH
ENV CRONET_GO_PATH=${CRONET_GO_PATH}
RUN set -ex \ RUN set -ex \
&& apk add git build-base \ && git config --global --add safe.directory /go/src/github.com/sagernet/sing-box \
&& export COMMIT=$(git rev-parse --short HEAD) \ && export GOTOOLCHAIN=local \
&& export VERSION=$(go run ./cmd/internal/read_tag) \ && export CGO_ENABLED=1 GOOS=$TARGETOS GOARCH=$TARGETARCH \
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_OTHERS) \ && eval "$(CGO_ENABLED=0 GOOS= GOARCH= go run -C "$CRONET_GO_PATH" ./cmd/build-naive --target=$GOOS/$GOARCH --libc=musl env --export)" \
&& export CGO_LDFLAGS="$CGO_LDFLAGS -Wl,-z,notext -Wl,-z,execstack" \
&& export VERSION=$(CGO_ENABLED=0 GOOS= GOARCH= go run ./cmd/internal/read_tag) \
&& export TAGS=$(cat release/DEFAULT_BUILD_TAGS_DOCKER) \
&& export LDFLAGS_SHARED=$(cat release/LDFLAGS) \ && export LDFLAGS_SHARED=$(cat release/LDFLAGS) \
&& go build -v -trimpath -tags "$TAGS" \ && go build -v -trimpath -tags "$TAGS" \
-o /go/bin/sing-box \ -o /go/bin/sing-box \

View File

@@ -3,7 +3,7 @@
[![license](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE) [![license](https://img.shields.io/badge/license-GPLv3-blue.svg)](LICENSE)
[![go](https://img.shields.io/badge/go-1.26-00ADD8.svg)](go.mod) [![go](https://img.shields.io/badge/go-1.26-00ADD8.svg)](go.mod)
[![codeberg](https://img.shields.io/badge/mirror-codeberg-2185D0.svg)](https://codeberg.org/shtorm-7/sing-box-extended) [![codeberg](https://img.shields.io/badge/mirror-codeberg-2185D0.svg)](https://codeberg.org/shtorm-7/sing-box-extended)
[![telegram](https://img.shields.io/badge/telegram-chat-26A5E4.svg)](https://t.me/sing_box_extended) [![telegram](https://img.shields.io/badge/telegram-chat-26A5E4.svg?logo=telegram)](https://t.me/sing_box_extended)
Sing-box with extended features. Sing-box with extended features.

View File

@@ -29,8 +29,8 @@ type Conn struct {
func (c *Conn) Close() error { func (c *Conn) Close() error {
c.group.access.Lock() c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element) c.group.connections.Remove(c.element)
c.group.access.Unlock()
return c.Conn.Close() return c.Conn.Close()
} }
@@ -58,8 +58,8 @@ type PacketConn struct {
func (c *PacketConn) Close() error { func (c *PacketConn) Close() error {
c.group.access.Lock() c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element) c.group.connections.Remove(c.element)
c.group.access.Unlock()
return c.PacketConn.Close() return c.PacketConn.Close()
} }
@@ -87,8 +87,8 @@ type SingPacketConn struct {
func (c *SingPacketConn) Close() error { func (c *SingPacketConn) Close() error {
c.group.access.Lock() c.group.access.Lock()
defer c.group.access.Unlock()
c.group.connections.Remove(c.element) c.group.connections.Remove(c.element)
c.group.access.Unlock()
return c.PacketConn.Close() return c.PacketConn.Close()
} }

View File

@@ -47,15 +47,19 @@ func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool, isProvider
func (g *Group) Interrupt(interruptExternalConnections bool) { func (g *Group) Interrupt(interruptExternalConnections bool) {
g.access.Lock() g.access.Lock()
defer g.access.Unlock()
var toDelete []*list.Element[*groupConnItem] var toDelete []*list.Element[*groupConnItem]
var toClose []io.Closer
for element := g.connections.Front(); element != nil; element = element.Next() { for element := g.connections.Front(); element != nil; element = element.Next() {
if !element.Value.isExternal || interruptExternalConnections { if !element.Value.isExternal || interruptExternalConnections {
element.Value.conn.Close()
toDelete = append(toDelete, element) toDelete = append(toDelete, element)
toClose = append(toClose, element.Value.conn)
} }
} }
for _, element := range toDelete { for _, element := range toDelete {
g.connections.Remove(element) g.connections.Remove(element)
} }
g.access.Unlock()
for _, conn := range toClose {
_ = conn.Close()
}
} }

View File

@@ -0,0 +1,66 @@
package interrupt
import (
"net"
"sync"
"testing"
"time"
)
type closeBarrierConn struct {
net.Conn
barrier *sync.WaitGroup
}
func (c *closeBarrierConn) Close() error {
c.barrier.Done()
c.barrier.Wait()
return c.Conn.Close()
}
func TestNestedGroupsInterruptWithoutDeadlock(t *testing.T) {
groupA := NewGroup()
groupB := NewGroup()
barrier := &sync.WaitGroup{}
barrier.Add(2)
barrierA, barrierAPeer := net.Pipe()
barrierB, barrierBPeer := net.Pipe()
t.Cleanup(func() {
barrierAPeer.Close()
barrierBPeer.Close()
})
groupA.NewConn(&closeBarrierConn{Conn: barrierA, barrier: barrier}, true, false)
groupB.NewConn(&closeBarrierConn{Conn: barrierB, barrier: barrier}, true, false)
connA, connAPeer := net.Pipe()
connB, connBPeer := net.Pipe()
t.Cleanup(func() {
connAPeer.Close()
connBPeer.Close()
})
wrapperA := groupA.NewConn(connA, true, false)
wrapperB := groupB.NewConn(connB, true, false)
groupA.NewConn(wrapperB, true, false)
groupB.NewConn(wrapperA, true, false)
done := make(chan struct{}, 2)
go func() {
groupA.Interrupt(true)
done <- struct{}{}
}()
go func() {
groupB.Interrupt(true)
done <- struct{}{}
}()
timeout := time.NewTimer(time.Second)
defer timeout.Stop()
for range 2 {
select {
case <-done:
case <-timeout.C:
t.Fatal("nested group interrupt deadlocked")
}
}
}

View File

@@ -168,9 +168,9 @@ func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn
} }
binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix())) binary.BigEndian.PutUint64(hello.SessionId, uint64(nowTime.Unix()))
hello.SessionId[0] = 1 hello.SessionId[0] = 26
hello.SessionId[1] = 8 hello.SessionId[1] = 7
hello.SessionId[2] = 1 hello.SessionId[2] = 11
binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix())) binary.BigEndian.PutUint32(hello.SessionId[4:], uint32(time.Now().Unix()))
copy(hello.SessionId[8:], e.shortID[:]) copy(hello.SessionId[8:], e.shortID[:])
if debug.Enabled { if debug.Enabled {

View File

@@ -24,7 +24,7 @@ type FallbackOutboundOptions struct {
type GroupCommonOption struct { type GroupCommonOption struct {
Outbounds []string `json:"outbounds"` Outbounds []string `json:"outbounds"`
Providers []string `json:"providers"` Providers badoption.Listable[string] `json:"providers,omitempty"`
Exclude *badoption.Regexp `json:"exclude,omitempty"` Exclude *badoption.Regexp `json:"exclude,omitempty"`
Include *badoption.Regexp `json:"include,omitempty"` Include *badoption.Regexp `json:"include,omitempty"`
UseAllProviders bool `json:"use_all_providers,omitempty"` UseAllProviders bool `json:"use_all_providers,omitempty"`

View File

@@ -102,10 +102,9 @@ func (n *Inbound) Start(stage adapter.StartStage) error {
} }
func (n *Inbound) Close() error { func (n *Inbound) Close() error {
err := common.Close(&n.listener)
n.proxy.Shutdown() n.proxy.Shutdown()
return common.Close( return err
&n.listener,
)
} }
func (h *Inbound) UpdateUsers(users []option.MTProxyUser) { func (h *Inbound) UpdateUsers(users []option.MTProxyUser) {

View File

@@ -94,18 +94,7 @@ func (u *clientPacketConn) writePacketToServer(buffer *buf.Buffer, source M.Sock
common.Must(binary.Write(header, binary.BigEndian, source.Port)) common.Must(binary.Write(header, binary.BigEndian, source.Port))
common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName)))) common.Must(binary.Write(header, binary.BigEndian, uint8(len(appName))))
common.Must1(header.WriteString(appName)) common.Must1(header.WriteString(appName))
_, err := u.writer.Write(header.Bytes()) return u.writeChunks(header.Bytes(), buffer.Bytes())
if err != nil {
return err
}
_, err = u.writer.Write(buffer.Bytes())
if err != nil {
return err
}
if u.flusher != nil {
u.flusher.Flush()
}
return nil
} }
var ( var (
@@ -195,16 +184,5 @@ func (u *serverPacketConn) writePacketToClient(buffer *buf.Buffer, source M.Sock
common.Must1(header.Write(sourceAddress[:])) common.Must1(header.Write(sourceAddress[:]))
common.Must(binary.Write(header, binary.BigEndian, source.Port)) common.Must(binary.Write(header, binary.BigEndian, source.Port))
common.Must(header.WriteZeroN(16 + 2)) common.Must(header.WriteZeroN(16 + 2))
_, err := u.writer.Write(header.Bytes()) return u.writeChunks(header.Bytes(), buffer.Bytes())
if err != nil {
return err
}
_, err = u.writer.Write(buffer.Bytes())
if err != nil {
return err
}
if u.flusher != nil {
u.flusher.Flush()
}
return nil
} }

View File

@@ -71,6 +71,9 @@ type httpConn struct {
localAddr net.Addr localAddr net.Addr
deadline *time.Timer deadline *time.Timer
done chan struct{} done chan struct{}
closed bool
mtx sync.Mutex
} }
func (h *httpConn) setup(body io.ReadCloser, err error) { func (h *httpConn) setup(body io.ReadCloser, err error) {
@@ -93,6 +96,9 @@ func (h *httpConn) waitCreated() error {
} }
func (h *httpConn) Close() error { func (h *httpConn) Close() error {
h.mtx.Lock()
h.closed = true
h.mtx.Unlock()
h.setup(nil, net.ErrClosed) h.setup(nil, net.ErrClosed)
if closer, ok := h.writer.(io.Closer); ok { if closer, ok := h.writer.(io.Closer); ok {
_ = closer.Close() _ = closer.Close()
@@ -117,11 +123,28 @@ func (h *httpConn) Close() error {
} }
func (h *httpConn) writeFlush(p []byte) (n int, err error) { func (h *httpConn) writeFlush(p []byte) (n int, err error) {
n, err = h.writer.Write(p) err = h.writeChunks(p)
if err != nil {
return 0, err
}
return len(p), nil
}
func (h *httpConn) writeChunks(chunks ...[]byte) error {
h.mtx.Lock()
defer h.mtx.Unlock()
if h.closed {
return net.ErrClosed
}
for _, chunk := range chunks {
if _, err := h.writer.Write(chunk); err != nil {
return err
}
}
if h.flusher != nil { if h.flusher != nil {
h.flusher.Flush() h.flusher.Flush()
} }
return n, err return nil
} }
func (h *httpConn) RemoteAddr() net.Addr { func (h *httpConn) RemoteAddr() net.Addr {

View File

@@ -348,6 +348,7 @@ func createHTTPClient(ctx context.Context, dest M.Socksaddr, dialer N.Dialer, op
} }
conn, dErr := qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsConfig, cfg) conn, dErr := qtls.DialEarly(ctx, bufio.NewUnbindPacketConn(udpConn), udpConn.RemoteAddr(), tlsConfig, cfg)
if dErr != nil { if dErr != nil {
_ = udpConn.Close()
return nil, dErr return nil, dErr
} }
if congestionControlFactory != nil { if congestionControlFactory != nil {