mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-07-27 01:08:29 +03:00
Compare commits
29 Commits
v1.13.14-e
...
v1.12.8-ex
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc91312a73 | ||
|
|
1d603e24fd | ||
|
|
9dc526ea1f | ||
|
|
93eb435e26 | ||
|
|
e22416f0d9 | ||
|
|
89497dbfd5 | ||
|
|
8388abbb77 | ||
|
|
180b7c4134 | ||
|
|
deda2cca5e | ||
|
|
691ecd45a9 | ||
|
|
209b89a4a3 | ||
|
|
765111a552 | ||
|
|
824eac453b | ||
|
|
85a1a8a53b | ||
|
|
ae9e7aa5f4 | ||
|
|
52b71c6f00 | ||
|
|
5d04673783 | ||
|
|
307c429715 | ||
|
|
6801b58d96 | ||
|
|
6272596ebc | ||
|
|
7c4c2d5ca8 | ||
|
|
6768c77fa0 | ||
|
|
0dff811977 | ||
|
|
e1a58d12fe | ||
|
|
4e74a4108d | ||
|
|
0238c54261 | ||
|
|
5c911c97d8 | ||
|
|
2cfc8092ad | ||
|
|
26bd698462 |
20
README.md
20
README.md
@@ -1,12 +1,20 @@
|
|||||||
# sing-box
|
# sing-box-extended
|
||||||
|
|
||||||
The universal proxy platform.
|
Sing-box with extended features.
|
||||||
|
|
||||||
[](https://repology.org/project/sing-box/versions)
|
## Features
|
||||||
|
|
||||||
## Documentation
|
* Amnezia
|
||||||
|
* WARP
|
||||||
|
* Tunneling
|
||||||
|
* Mieru
|
||||||
|
* XHTTP
|
||||||
|
* SDNS (DNSCrypt)
|
||||||
|
* Unified delay
|
||||||
|
|
||||||
https://sing-box.sagernet.org
|
## Examples
|
||||||
|
|
||||||
|
https://github.com/shtorm-7/sing-box-extended/tree/extended/examples
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@@ -28,4 +36,4 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
In addition, no derivative work may use the name or imply association
|
In addition, no derivative work may use the name or imply association
|
||||||
with this application without prior consent.
|
with this application without prior consent.
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ func NewManager(logger log.ContextLogger, registry adapter.EndpointRegistry) *Ma
|
|||||||
|
|
||||||
func (m *Manager) Start(stage adapter.StartStage) error {
|
func (m *Manager) Start(stage adapter.StartStage) error {
|
||||||
m.access.Lock()
|
m.access.Lock()
|
||||||
defer m.access.Unlock()
|
|
||||||
if m.started && m.stage >= stage {
|
if m.started && m.stage >= stage {
|
||||||
panic("already started")
|
panic("already started")
|
||||||
}
|
}
|
||||||
@@ -43,9 +42,12 @@ func (m *Manager) Start(stage adapter.StartStage) error {
|
|||||||
m.stage = stage
|
m.stage = stage
|
||||||
if stage == adapter.StartStateStart {
|
if stage == adapter.StartStateStart {
|
||||||
// started with outbound manager
|
// started with outbound manager
|
||||||
|
m.access.Unlock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for _, endpoint := range m.endpoints {
|
endpoints := m.endpoints
|
||||||
|
m.access.Unlock()
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
err := adapter.LegacyStart(endpoint, stage)
|
err := adapter.LegacyStart(endpoint, stage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
return E.Cause(err, stage, " endpoint/", endpoint.Type(), "[", endpoint.Tag(), "]")
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ type CacheFile interface {
|
|||||||
StoreRDRC() bool
|
StoreRDRC() bool
|
||||||
RDRCStore
|
RDRCStore
|
||||||
|
|
||||||
|
StoreWARPConfig() bool
|
||||||
|
|
||||||
LoadMode() string
|
LoadMode() string
|
||||||
StoreMode(mode string) error
|
StoreMode(mode string) error
|
||||||
LoadSelected(group string) string
|
LoadSelected(group string) string
|
||||||
@@ -52,6 +54,8 @@ type CacheFile interface {
|
|||||||
StoreGroupExpand(group string, expand bool) error
|
StoreGroupExpand(group string, expand bool) error
|
||||||
LoadRuleSet(tag string) *SavedBinary
|
LoadRuleSet(tag string) *SavedBinary
|
||||||
SaveRuleSet(tag string, set *SavedBinary) error
|
SaveRuleSet(tag string, set *SavedBinary) error
|
||||||
|
LoadWARPConfig(tag string) *SavedBinary
|
||||||
|
SaveWARPConfig(tag string, set *SavedBinary) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavedBinary struct {
|
type SavedBinary struct {
|
||||||
|
|||||||
@@ -42,14 +42,16 @@ type InboundManager interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type InboundContext struct {
|
type InboundContext struct {
|
||||||
Inbound string
|
Inbound string
|
||||||
InboundType string
|
InboundType string
|
||||||
IPVersion uint8
|
IPVersion uint8
|
||||||
Network string
|
Network string
|
||||||
Source M.Socksaddr
|
Source M.Socksaddr
|
||||||
Destination M.Socksaddr
|
Destination M.Socksaddr
|
||||||
User string
|
TunnelSource string
|
||||||
Outbound string
|
TunnelDestination string
|
||||||
|
User string
|
||||||
|
Outbound string
|
||||||
|
|
||||||
// sniffer
|
// sniffer
|
||||||
|
|
||||||
|
|||||||
4
box.go
4
box.go
@@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/common/dialer"
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
"github.com/sagernet/sing-box/common/taskmonitor"
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
"github.com/sagernet/sing-box/common/tls"
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/sagernet/sing-box/common/urltest"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
"github.com/sagernet/sing-box/dns"
|
"github.com/sagernet/sing-box/dns"
|
||||||
"github.com/sagernet/sing-box/dns/transport/local"
|
"github.com/sagernet/sing-box/dns/transport/local"
|
||||||
@@ -139,6 +140,9 @@ func New(options Options) (*Box, error) {
|
|||||||
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
needV2RayAPI = true
|
needV2RayAPI = true
|
||||||
}
|
}
|
||||||
|
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
|
||||||
|
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
|
||||||
|
}
|
||||||
platformInterface := service.FromContext[platform.Interface](ctx)
|
platformInterface := service.FromContext[platform.Interface](ctx)
|
||||||
var defaultLogWriter io.Writer
|
var defaultLogWriter io.Writer
|
||||||
if platformInterface != nil {
|
if platformInterface != nil {
|
||||||
|
|||||||
74
common/cloudflare/api.go
Normal file
74
common/cloudflare/api.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudflareApi struct {
|
||||||
|
client http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCloudflareApi(opts ...CloudflareApiOption) *CloudflareApi {
|
||||||
|
api := &CloudflareApi{http.Client{Timeout: 30 * time.Second}}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(api)
|
||||||
|
}
|
||||||
|
return api
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *CloudflareApi) CreateProfile(ctx context.Context, publicKey string) (*CloudflareProfile, error) {
|
||||||
|
request, err := http.NewRequest("POST", "https://api.cloudflareclient.com/v0i1909051800/reg", strings.NewReader(
|
||||||
|
fmt.Sprintf(
|
||||||
|
"{\"install_id\":\"\",\"tos\":\"%s\",\"key\":\"%s\",\"fcm_token\":\"\",\"type\":\"ios\",\"locale\":\"en_US\"}",
|
||||||
|
time.Now().Format("2006-01-02T15:04:05.000Z"),
|
||||||
|
publicKey,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
response, err := api.client.Do(request.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("status code is not 200")
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
profile := new(CloudflareProfile)
|
||||||
|
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *CloudflareApi) GetProfile(ctx context.Context, authToken string, id string) (*CloudflareProfile, error) {
|
||||||
|
request, err := http.NewRequest("GET", "https://api.cloudflareclient.com/v0i1909051800/reg/"+id, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Header.Set("Authorization", "Bearer "+authToken)
|
||||||
|
response, err := api.client.Do(request.WithContext(ctx))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
if response.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("status code is not 200")
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
profile := new(CloudflareProfile)
|
||||||
|
return profile, json.NewDecoder(strings.NewReader(gjson.Get(string(content), "result").Raw)).Decode(profile)
|
||||||
|
}
|
||||||
17
common/cloudflare/option.go
Normal file
17
common/cloudflare/option.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CloudflareApiOption func(api *CloudflareApi)
|
||||||
|
|
||||||
|
func WithDialContext(dialContext func(ctx context.Context, network, addr string) (net.Conn, error)) CloudflareApiOption {
|
||||||
|
return func(api *CloudflareApi) {
|
||||||
|
api.client.Transport = &http.Transport{
|
||||||
|
DialContext: dialContext,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
common/cloudflare/profile.go
Normal file
64
common/cloudflare/profile.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package cloudflare
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type CloudflareProfile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Account struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
AccountType string `json:"account_type"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
PremiumData int `json:"premium_data"`
|
||||||
|
Quota int `json:"quota"`
|
||||||
|
Usage int `json:"usage"`
|
||||||
|
WARPPlus bool `json:"warp_plus"`
|
||||||
|
ReferralCount int `json:"referral_count"`
|
||||||
|
ReferralRenewalCountdown int `json:"referral_renewal_countdown"`
|
||||||
|
Role string `json:"role"`
|
||||||
|
License string `json:"license"`
|
||||||
|
TTL time.Time `json:"ttl"`
|
||||||
|
} `json:"account"`
|
||||||
|
Config struct {
|
||||||
|
ClientID string `json:"client_id"`
|
||||||
|
Interface struct {
|
||||||
|
Addresses struct {
|
||||||
|
V4 string `json:"v4"`
|
||||||
|
V6 string `json:"v6"`
|
||||||
|
} `json:"addresses"`
|
||||||
|
} `json:"interface"`
|
||||||
|
Peers []struct {
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
Endpoint struct {
|
||||||
|
V4 string `json:"v4"`
|
||||||
|
V6 string `json:"v6"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Ports []int `json:"ports"`
|
||||||
|
} `json:"endpoint"`
|
||||||
|
} `json:"peers"`
|
||||||
|
Services struct {
|
||||||
|
HTTPProxy string `json:"http_proxy"`
|
||||||
|
} `json:"services"`
|
||||||
|
Metrics struct {
|
||||||
|
Ping int `json:"ping"`
|
||||||
|
Report int `json:"report"`
|
||||||
|
} `json:"metrics"`
|
||||||
|
} `json:"config"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
WARPEnabled bool `json:"warp_enabled"`
|
||||||
|
WaitlistEnabled bool `json:"waitlist_enabled"`
|
||||||
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
|
Tos time.Time `json:"tos"`
|
||||||
|
Place int `json:"place"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
InstallID string `json:"install_id"`
|
||||||
|
FcmToken string `json:"fcm_token"`
|
||||||
|
Policy struct {
|
||||||
|
TunnelProtocol string `json:"tunnel_protocol"`
|
||||||
|
} `json:"policy"`
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package interrupt
|
|||||||
import (
|
import (
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -73,3 +74,32 @@ func (c *PacketConn) WriterReplaceable() bool {
|
|||||||
func (c *PacketConn) Upstream() any {
|
func (c *PacketConn) Upstream() any {
|
||||||
return c.PacketConn
|
return c.PacketConn
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SingPacketConn struct {
|
||||||
|
N.PacketConn
|
||||||
|
group *Group
|
||||||
|
element *list.Element[*groupConnItem]
|
||||||
|
}
|
||||||
|
|
||||||
|
/*func (c *SingPacketConn) MarkAsInternal() {
|
||||||
|
c.element.Value.internal = true
|
||||||
|
}*/
|
||||||
|
|
||||||
|
func (c *SingPacketConn) Close() error {
|
||||||
|
c.group.access.Lock()
|
||||||
|
defer c.group.access.Unlock()
|
||||||
|
c.group.connections.Remove(c.element)
|
||||||
|
return c.PacketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SingPacketConn) ReaderReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SingPacketConn) WriterReplaceable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SingPacketConn) Upstream() any {
|
||||||
|
return c.PacketConn
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
"github.com/sagernet/sing/common/x/list"
|
"github.com/sagernet/sing/common/x/list"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,6 +37,13 @@ func (g *Group) NewPacketConn(conn net.PacketConn, isExternal bool) net.PacketCo
|
|||||||
return &PacketConn{PacketConn: conn, group: g, element: item}
|
return &PacketConn{PacketConn: conn, group: g, element: item}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *Group) NewSingPacketConn(conn N.PacketConn, isExternal bool) N.PacketConn {
|
||||||
|
g.access.Lock()
|
||||||
|
defer g.access.Unlock()
|
||||||
|
item := g.connections.PushBack(&groupConnItem{conn, isExternal})
|
||||||
|
return &SingPacketConn{PacketConn: conn, group: g, element: item}
|
||||||
|
}
|
||||||
|
|
||||||
func (g *Group) Interrupt(interruptExternalConnections bool) {
|
func (g *Group) Interrupt(interruptExternalConnections bool) {
|
||||||
g.access.Lock()
|
g.access.Lock()
|
||||||
defer g.access.Unlock()
|
defer g.access.Unlock()
|
||||||
|
|||||||
13
common/urltest/context.go
Normal file
13
common/urltest/context.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package urltest
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type contextKeyIsUnifiedDelay struct{}
|
||||||
|
|
||||||
|
func ContextWithIsUnifiedDelay(ctx context.Context) context.Context {
|
||||||
|
return context.WithValue(ctx, contextKeyIsUnifiedDelay{}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsUnifiedDelayFromContext(ctx context.Context) bool {
|
||||||
|
return ctx.Value(contextKeyIsUnifiedDelay{}) != nil
|
||||||
|
}
|
||||||
@@ -128,6 +128,15 @@ func URLTest(ctx context.Context, link string, detour N.Dialer) (t uint16, err e
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
if IsUnifiedDelayFromContext(ctx) {
|
||||||
|
second := time.Now()
|
||||||
|
resp, err = client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
start = second
|
||||||
|
}
|
||||||
t = uint16(time.Since(start) / time.Millisecond)
|
t = uint16(time.Since(start) / time.Millisecond)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
337
common/xray/buf/buffer.go
Normal file
337
common/xray/buf/buffer.go
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/bytespool"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/net"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Size of a regular buffer.
|
||||||
|
Size = 8192
|
||||||
|
)
|
||||||
|
|
||||||
|
var zero = [Size * 10]byte{0}
|
||||||
|
|
||||||
|
var pool = bytespool.GetPool(Size)
|
||||||
|
|
||||||
|
// ownership represents the data owner of the buffer.
|
||||||
|
type ownership uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
managed ownership = iota
|
||||||
|
unmanaged
|
||||||
|
bytespools
|
||||||
|
)
|
||||||
|
|
||||||
|
// Buffer is a recyclable allocation of a byte array. Buffer.Release() recycles
|
||||||
|
// the buffer into an internal buffer pool, in order to recreate a buffer more
|
||||||
|
// quickly.
|
||||||
|
type Buffer struct {
|
||||||
|
v []byte
|
||||||
|
start int32
|
||||||
|
end int32
|
||||||
|
ownership ownership
|
||||||
|
UDP *net.Destination
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a Buffer with 0 length and 8K capacity, managed.
|
||||||
|
func New() *Buffer {
|
||||||
|
buf := pool.Get().([]byte)
|
||||||
|
if cap(buf) >= Size {
|
||||||
|
buf = buf[:Size]
|
||||||
|
} else {
|
||||||
|
buf = make([]byte, Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Buffer{
|
||||||
|
v: buf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewExisted creates a standard size Buffer with an existed bytearray, managed.
|
||||||
|
func NewExisted(b []byte) *Buffer {
|
||||||
|
if cap(b) < Size {
|
||||||
|
panic("Invalid buffer")
|
||||||
|
}
|
||||||
|
|
||||||
|
oLen := len(b)
|
||||||
|
if oLen < Size {
|
||||||
|
b = b[:Size]
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Buffer{
|
||||||
|
v: b,
|
||||||
|
end: int32(oLen),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromBytes creates a Buffer with an existed bytearray, unmanaged.
|
||||||
|
func FromBytes(b []byte) *Buffer {
|
||||||
|
return &Buffer{
|
||||||
|
v: b,
|
||||||
|
end: int32(len(b)),
|
||||||
|
ownership: unmanaged,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StackNew creates a new Buffer object on stack, managed.
|
||||||
|
// This method is for buffers that is released in the same function.
|
||||||
|
func StackNew() Buffer {
|
||||||
|
buf := pool.Get().([]byte)
|
||||||
|
if cap(buf) >= Size {
|
||||||
|
buf = buf[:Size]
|
||||||
|
} else {
|
||||||
|
buf = make([]byte, Size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Buffer{
|
||||||
|
v: buf,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithSize creates a Buffer with 0 length and capacity with at least the given size, bytespool's.
|
||||||
|
func NewWithSize(size int32) *Buffer {
|
||||||
|
return &Buffer{
|
||||||
|
v: bytespool.Alloc(size),
|
||||||
|
ownership: bytespools,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release recycles the buffer into an internal buffer pool.
|
||||||
|
func (b *Buffer) Release() {
|
||||||
|
if b == nil || b.v == nil || b.ownership == unmanaged {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p := b.v
|
||||||
|
b.v = nil
|
||||||
|
b.Clear()
|
||||||
|
|
||||||
|
switch b.ownership {
|
||||||
|
case managed:
|
||||||
|
if cap(p) == Size {
|
||||||
|
pool.Put(p)
|
||||||
|
}
|
||||||
|
case bytespools:
|
||||||
|
bytespool.Free(p)
|
||||||
|
}
|
||||||
|
b.UDP = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear clears the content of the buffer, results an empty buffer with
|
||||||
|
// Len() = 0.
|
||||||
|
func (b *Buffer) Clear() {
|
||||||
|
b.start = 0
|
||||||
|
b.end = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Byte returns the bytes at index.
|
||||||
|
func (b *Buffer) Byte(index int32) byte {
|
||||||
|
return b.v[b.start+index]
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetByte sets the byte value at index.
|
||||||
|
func (b *Buffer) SetByte(index int32, value byte) {
|
||||||
|
b.v[b.start+index] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the content bytes of this Buffer.
|
||||||
|
func (b *Buffer) Bytes() []byte {
|
||||||
|
return b.v[b.start:b.end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend increases the buffer size by n bytes, and returns the extended part.
|
||||||
|
// It panics if result size is larger than buf.Size.
|
||||||
|
func (b *Buffer) Extend(n int32) []byte {
|
||||||
|
end := b.end + n
|
||||||
|
if end > int32(len(b.v)) {
|
||||||
|
panic("extending out of bound")
|
||||||
|
}
|
||||||
|
ext := b.v[b.end:end]
|
||||||
|
b.end = end
|
||||||
|
copy(ext, zero[:])
|
||||||
|
return ext
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesRange returns a slice of this buffer with given from and to boundary.
|
||||||
|
func (b *Buffer) BytesRange(from, to int32) []byte {
|
||||||
|
if from < 0 {
|
||||||
|
from += b.Len()
|
||||||
|
}
|
||||||
|
if to < 0 {
|
||||||
|
to += b.Len()
|
||||||
|
}
|
||||||
|
return b.v[b.start+from : b.start+to]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesFrom returns a slice of this Buffer starting from the given position.
|
||||||
|
func (b *Buffer) BytesFrom(from int32) []byte {
|
||||||
|
if from < 0 {
|
||||||
|
from += b.Len()
|
||||||
|
}
|
||||||
|
return b.v[b.start+from : b.end]
|
||||||
|
}
|
||||||
|
|
||||||
|
// BytesTo returns a slice of this Buffer from start to the given position.
|
||||||
|
func (b *Buffer) BytesTo(to int32) []byte {
|
||||||
|
if to < 0 {
|
||||||
|
to += b.Len()
|
||||||
|
}
|
||||||
|
if to < 0 {
|
||||||
|
to = 0
|
||||||
|
}
|
||||||
|
return b.v[b.start : b.start+to]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check makes sure that 0 <= b.start <= b.end.
|
||||||
|
func (b *Buffer) Check() {
|
||||||
|
if b.start < 0 {
|
||||||
|
b.start = 0
|
||||||
|
}
|
||||||
|
if b.end < 0 {
|
||||||
|
b.end = 0
|
||||||
|
}
|
||||||
|
if b.start > b.end {
|
||||||
|
b.start = b.end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize cuts the buffer at the given position.
|
||||||
|
func (b *Buffer) Resize(from, to int32) {
|
||||||
|
oldEnd := b.end
|
||||||
|
if from < 0 {
|
||||||
|
from += b.Len()
|
||||||
|
}
|
||||||
|
if to < 0 {
|
||||||
|
to += b.Len()
|
||||||
|
}
|
||||||
|
if to < from {
|
||||||
|
panic("Invalid slice")
|
||||||
|
}
|
||||||
|
b.end = b.start + to
|
||||||
|
b.start += from
|
||||||
|
b.Check()
|
||||||
|
if b.end > oldEnd {
|
||||||
|
copy(b.v[oldEnd:b.end], zero[:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Advance cuts the buffer at the given position.
|
||||||
|
func (b *Buffer) Advance(from int32) {
|
||||||
|
if from < 0 {
|
||||||
|
from += b.Len()
|
||||||
|
}
|
||||||
|
b.start += from
|
||||||
|
b.Check()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the length of the buffer content.
|
||||||
|
func (b *Buffer) Len() int32 {
|
||||||
|
if b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return b.end - b.start
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cap returns the capacity of the buffer content.
|
||||||
|
func (b *Buffer) Cap() int32 {
|
||||||
|
if b == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int32(len(b.v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the buffer is empty.
|
||||||
|
func (b *Buffer) IsEmpty() bool {
|
||||||
|
return b.Len() == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsFull returns true if the buffer has no more room to grow.
|
||||||
|
func (b *Buffer) IsFull() bool {
|
||||||
|
return b != nil && b.end == int32(len(b.v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements Write method in io.Writer.
|
||||||
|
func (b *Buffer) Write(data []byte) (int, error) {
|
||||||
|
nBytes := copy(b.v[b.end:], data)
|
||||||
|
b.end += int32(nBytes)
|
||||||
|
return nBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteByte writes a single byte into the buffer.
|
||||||
|
func (b *Buffer) WriteByte(v byte) error {
|
||||||
|
if b.IsFull() {
|
||||||
|
return E.New("buffer full")
|
||||||
|
}
|
||||||
|
b.v[b.end] = v
|
||||||
|
b.end++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteString implements io.StringWriter.
|
||||||
|
func (b *Buffer) WriteString(s string) (int, error) {
|
||||||
|
return b.Write([]byte(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadByte implements io.ByteReader
|
||||||
|
func (b *Buffer) ReadByte() (byte, error) {
|
||||||
|
if b.start == b.end {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
nb := b.v[b.start]
|
||||||
|
b.start++
|
||||||
|
return nb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBytes implements bufio.Reader.ReadBytes
|
||||||
|
func (b *Buffer) ReadBytes(length int32) ([]byte, error) {
|
||||||
|
if b.end-b.start < length {
|
||||||
|
return nil, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
nb := b.v[b.start : b.start+length]
|
||||||
|
b.start += length
|
||||||
|
return nb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader.Read().
|
||||||
|
func (b *Buffer) Read(data []byte) (int, error) {
|
||||||
|
if b.Len() == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
nBytes := copy(data, b.v[b.start:b.end])
|
||||||
|
if int32(nBytes) == b.Len() {
|
||||||
|
b.Clear()
|
||||||
|
} else {
|
||||||
|
b.start += int32(nBytes)
|
||||||
|
}
|
||||||
|
return nBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom implements io.ReaderFrom.
|
||||||
|
func (b *Buffer) ReadFrom(reader io.Reader) (int64, error) {
|
||||||
|
n, err := reader.Read(b.v[b.end:])
|
||||||
|
b.end += int32(n)
|
||||||
|
return int64(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFullFrom reads exact size of bytes from given reader, or until error occurs.
|
||||||
|
func (b *Buffer) ReadFullFrom(reader io.Reader, size int32) (int64, error) {
|
||||||
|
end := b.end + size
|
||||||
|
if end > int32(len(b.v)) {
|
||||||
|
v := end
|
||||||
|
return 0, E.New("out of bound: ", v)
|
||||||
|
}
|
||||||
|
n, err := io.ReadFull(reader, b.v[b.end:end])
|
||||||
|
b.end += int32(n)
|
||||||
|
return int64(n), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string form of this Buffer.
|
||||||
|
func (b *Buffer) String() string {
|
||||||
|
return string(b.Bytes())
|
||||||
|
}
|
||||||
124
common/xray/buf/copy.go
Normal file
124
common/xray/buf/copy.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/errors"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dataHandler func(MultiBuffer)
|
||||||
|
|
||||||
|
type copyHandler struct {
|
||||||
|
onData []dataHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
// SizeCounter is for counting bytes copied by Copy().
|
||||||
|
type SizeCounter struct {
|
||||||
|
Size int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// CopyOption is an option for copying data.
|
||||||
|
type CopyOption func(*copyHandler)
|
||||||
|
|
||||||
|
// UpdateActivity is a CopyOption to update activity on each data copy operation.
|
||||||
|
func UpdateActivity(timer signal.ActivityUpdater) CopyOption {
|
||||||
|
return func(handler *copyHandler) {
|
||||||
|
handler.onData = append(handler.onData, func(MultiBuffer) {
|
||||||
|
timer.Update()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountSize is a CopyOption that sums the total size of data copied into the given SizeCounter.
|
||||||
|
func CountSize(sc *SizeCounter) CopyOption {
|
||||||
|
return func(handler *copyHandler) {
|
||||||
|
handler.onData = append(handler.onData, func(b MultiBuffer) {
|
||||||
|
sc.Size += int64(b.Len())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type readError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e readError) Error() string {
|
||||||
|
return e.error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e readError) Unwrap() error {
|
||||||
|
return e.error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsReadError returns true if the error in Copy() comes from reading.
|
||||||
|
func IsReadError(err error) bool {
|
||||||
|
_, ok := err.(readError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type writeError struct {
|
||||||
|
error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e writeError) Error() string {
|
||||||
|
return e.error.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e writeError) Unwrap() error {
|
||||||
|
return e.error
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsWriteError returns true if the error in Copy() comes from writing.
|
||||||
|
func IsWriteError(err error) bool {
|
||||||
|
_, ok := err.(writeError)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyInternal(reader Reader, writer Writer, handler *copyHandler) error {
|
||||||
|
for {
|
||||||
|
buffer, err := reader.ReadMultiBuffer()
|
||||||
|
if !buffer.IsEmpty() {
|
||||||
|
for _, handler := range handler.onData {
|
||||||
|
handler(buffer)
|
||||||
|
}
|
||||||
|
|
||||||
|
if werr := writer.WriteMultiBuffer(buffer); werr != nil {
|
||||||
|
return writeError{werr}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return readError{err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy dumps all payload from reader to writer or stops when an error occurs. It returns nil when EOF.
|
||||||
|
func Copy(reader Reader, writer Writer, options ...CopyOption) error {
|
||||||
|
var handler copyHandler
|
||||||
|
for _, option := range options {
|
||||||
|
option(&handler)
|
||||||
|
}
|
||||||
|
err := copyInternal(reader, writer, &handler)
|
||||||
|
if err != nil && errors.Cause(err) != io.EOF {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ErrNotTimeoutReader = E.New("not a TimeoutReader")
|
||||||
|
|
||||||
|
func CopyOnceTimeout(reader Reader, writer Writer, timeout time.Duration) error {
|
||||||
|
timeoutReader, ok := reader.(TimeoutReader)
|
||||||
|
if !ok {
|
||||||
|
return ErrNotTimeoutReader
|
||||||
|
}
|
||||||
|
mb, err := timeoutReader.ReadMultiBufferTimeout(timeout)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writer.WriteMultiBuffer(mb)
|
||||||
|
}
|
||||||
126
common/xray/buf/io.go
Normal file
126
common/xray/buf/io.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/stat"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/stats"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reader extends io.Reader with MultiBuffer.
|
||||||
|
type Reader interface {
|
||||||
|
// ReadMultiBuffer reads content from underlying reader, and put it into a MultiBuffer.
|
||||||
|
ReadMultiBuffer() (MultiBuffer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrReadTimeout is an error that happens with IO timeout.
|
||||||
|
var ErrReadTimeout = E.New("IO timeout")
|
||||||
|
|
||||||
|
// TimeoutReader is a reader that returns error if Read() operation takes longer than the given timeout.
|
||||||
|
type TimeoutReader interface {
|
||||||
|
ReadMultiBufferTimeout(time.Duration) (MultiBuffer, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer extends io.Writer with MultiBuffer.
|
||||||
|
type Writer interface {
|
||||||
|
// WriteMultiBuffer writes a MultiBuffer into underlying writer.
|
||||||
|
WriteMultiBuffer(MultiBuffer) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAllBytes ensures all bytes are written into the given writer.
|
||||||
|
func WriteAllBytes(writer io.Writer, payload []byte, c stats.Counter) error {
|
||||||
|
wc := 0
|
||||||
|
defer func() {
|
||||||
|
if c != nil {
|
||||||
|
c.Add(int64(wc))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for len(payload) > 0 {
|
||||||
|
n, err := writer.Write(payload)
|
||||||
|
wc += n
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
payload = payload[n:]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPacketReader(reader io.Reader) bool {
|
||||||
|
_, ok := reader.(net.PacketConn)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReader creates a new Reader.
|
||||||
|
// The Reader instance doesn't take the ownership of reader.
|
||||||
|
func NewReader(reader io.Reader) Reader {
|
||||||
|
if mr, ok := reader.(Reader); ok {
|
||||||
|
return mr
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPacketReader(reader) {
|
||||||
|
return &PacketReader{
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SingleReader{
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPacketReader creates a new PacketReader based on the given reader.
|
||||||
|
func NewPacketReader(reader io.Reader) Reader {
|
||||||
|
if mr, ok := reader.(Reader); ok {
|
||||||
|
return mr
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PacketReader{
|
||||||
|
Reader: reader,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPacketWriter(writer io.Writer) bool {
|
||||||
|
if _, ok := writer.(net.PacketConn); ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the writer doesn't implement syscall.Conn, it is probably not a TCP connection.
|
||||||
|
if _, ok := writer.(syscall.Conn); !ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWriter creates a new Writer.
|
||||||
|
func NewWriter(writer io.Writer) Writer {
|
||||||
|
if mw, ok := writer.(Writer); ok {
|
||||||
|
return mw
|
||||||
|
}
|
||||||
|
|
||||||
|
iConn := writer
|
||||||
|
if statConn, ok := writer.(*stat.CounterConnection); ok {
|
||||||
|
iConn = statConn.Connection
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPacketWriter(iConn) {
|
||||||
|
return &SequentialWriter{
|
||||||
|
Writer: writer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter stats.Counter
|
||||||
|
|
||||||
|
if statConn, ok := writer.(*stat.CounterConnection); ok {
|
||||||
|
counter = statConn.WriteCounter
|
||||||
|
}
|
||||||
|
return &BufferToBytesWriter{
|
||||||
|
Writer: iConn,
|
||||||
|
counter: counter,
|
||||||
|
}
|
||||||
|
}
|
||||||
310
common/xray/buf/multi_buffer.go
Normal file
310
common/xray/buf/multi_buffer.go
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/errors"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/serial"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadAllToBytes reads all content from the reader into a byte array, until EOF.
|
||||||
|
func ReadAllToBytes(reader io.Reader) ([]byte, error) {
|
||||||
|
mb, err := ReadFrom(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if mb.Len() == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
b := make([]byte, mb.Len())
|
||||||
|
mb, _ = SplitBytes(mb, b)
|
||||||
|
ReleaseMulti(mb)
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiBuffer is a list of Buffers. The order of Buffer matters.
|
||||||
|
type MultiBuffer []*Buffer
|
||||||
|
|
||||||
|
// MergeMulti merges content from src to dest, and returns the new address of dest and src
|
||||||
|
func MergeMulti(dest MultiBuffer, src MultiBuffer) (MultiBuffer, MultiBuffer) {
|
||||||
|
dest = append(dest, src...)
|
||||||
|
for idx := range src {
|
||||||
|
src[idx] = nil
|
||||||
|
}
|
||||||
|
return dest, src[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// MergeBytes merges the given bytes into MultiBuffer and return the new address of the merged MultiBuffer.
|
||||||
|
func MergeBytes(dest MultiBuffer, src []byte) MultiBuffer {
|
||||||
|
n := len(dest)
|
||||||
|
if n > 0 && !(dest)[n-1].IsFull() {
|
||||||
|
nBytes, _ := (dest)[n-1].Write(src)
|
||||||
|
src = src[nBytes:]
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(src) > 0 {
|
||||||
|
b := New()
|
||||||
|
nBytes, _ := b.Write(src)
|
||||||
|
src = src[nBytes:]
|
||||||
|
dest = append(dest, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
return dest
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseMulti releases all content of the MultiBuffer, and returns an empty MultiBuffer.
|
||||||
|
func ReleaseMulti(mb MultiBuffer) MultiBuffer {
|
||||||
|
for i := range mb {
|
||||||
|
mb[i].Release()
|
||||||
|
mb[i] = nil
|
||||||
|
}
|
||||||
|
return mb[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy copied the beginning part of the MultiBuffer into the given byte array.
|
||||||
|
func (mb MultiBuffer) Copy(b []byte) int {
|
||||||
|
total := 0
|
||||||
|
for _, bb := range mb {
|
||||||
|
nBytes := copy(b[total:], bb.Bytes())
|
||||||
|
total += nBytes
|
||||||
|
if int32(nBytes) < bb.Len() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom reads all content from reader until EOF.
|
||||||
|
func ReadFrom(reader io.Reader) (MultiBuffer, error) {
|
||||||
|
mb := make(MultiBuffer, 0, 16)
|
||||||
|
for {
|
||||||
|
b := New()
|
||||||
|
_, err := b.ReadFullFrom(reader, Size)
|
||||||
|
if b.IsEmpty() {
|
||||||
|
b.Release()
|
||||||
|
} else {
|
||||||
|
mb = append(mb, b)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) == io.EOF || errors.Cause(err) == io.ErrUnexpectedEOF {
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitBytes splits the given amount of bytes from the beginning of the MultiBuffer.
|
||||||
|
// It returns the new address of MultiBuffer leftover, and number of bytes written into the input byte slice.
|
||||||
|
func SplitBytes(mb MultiBuffer, b []byte) (MultiBuffer, int) {
|
||||||
|
totalBytes := 0
|
||||||
|
endIndex := -1
|
||||||
|
for i := range mb {
|
||||||
|
pBuffer := mb[i]
|
||||||
|
nBytes, _ := pBuffer.Read(b)
|
||||||
|
totalBytes += nBytes
|
||||||
|
b = b[nBytes:]
|
||||||
|
if !pBuffer.IsEmpty() {
|
||||||
|
endIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
pBuffer.Release()
|
||||||
|
mb[i] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if endIndex == -1 {
|
||||||
|
mb = mb[:0]
|
||||||
|
} else {
|
||||||
|
mb = mb[endIndex:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return mb, totalBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitFirstBytes splits the first buffer from MultiBuffer, and then copy its content into the given slice.
|
||||||
|
func SplitFirstBytes(mb MultiBuffer, p []byte) (MultiBuffer, int) {
|
||||||
|
mb, b := SplitFirst(mb)
|
||||||
|
if b == nil {
|
||||||
|
return mb, 0
|
||||||
|
}
|
||||||
|
n := copy(p, b.Bytes())
|
||||||
|
b.Release()
|
||||||
|
return mb, n
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compact returns another MultiBuffer by merging all content of the given one together.
|
||||||
|
func Compact(mb MultiBuffer) MultiBuffer {
|
||||||
|
if len(mb) == 0 {
|
||||||
|
return mb
|
||||||
|
}
|
||||||
|
|
||||||
|
mb2 := make(MultiBuffer, 0, len(mb))
|
||||||
|
last := mb[0]
|
||||||
|
|
||||||
|
for i := 1; i < len(mb); i++ {
|
||||||
|
curr := mb[i]
|
||||||
|
if last.Len()+curr.Len() > Size {
|
||||||
|
mb2 = append(mb2, last)
|
||||||
|
last = curr
|
||||||
|
} else {
|
||||||
|
common.Must2(last.ReadFrom(curr))
|
||||||
|
curr.Release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mb2 = append(mb2, last)
|
||||||
|
return mb2
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitFirst splits the first Buffer from the beginning of the MultiBuffer.
|
||||||
|
func SplitFirst(mb MultiBuffer) (MultiBuffer, *Buffer) {
|
||||||
|
if len(mb) == 0 {
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := mb[0]
|
||||||
|
mb[0] = nil
|
||||||
|
mb = mb[1:]
|
||||||
|
return mb, b
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitSize splits the beginning of the MultiBuffer into another one, for at most size bytes.
|
||||||
|
func SplitSize(mb MultiBuffer, size int32) (MultiBuffer, MultiBuffer) {
|
||||||
|
if len(mb) == 0 {
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if mb[0].Len() > size {
|
||||||
|
b := New()
|
||||||
|
copy(b.Extend(size), mb[0].BytesTo(size))
|
||||||
|
mb[0].Advance(size)
|
||||||
|
return mb, MultiBuffer{b}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBytes := int32(0)
|
||||||
|
var r MultiBuffer
|
||||||
|
endIndex := -1
|
||||||
|
for i := range mb {
|
||||||
|
if totalBytes+mb[i].Len() > size {
|
||||||
|
endIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
totalBytes += mb[i].Len()
|
||||||
|
r = append(r, mb[i])
|
||||||
|
mb[i] = nil
|
||||||
|
}
|
||||||
|
if endIndex == -1 {
|
||||||
|
// To reuse mb array
|
||||||
|
mb = mb[:0]
|
||||||
|
} else {
|
||||||
|
mb = mb[endIndex:]
|
||||||
|
}
|
||||||
|
return mb, r
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitMulti splits the beginning of the MultiBuffer into first one, the index i and after into second one
|
||||||
|
func SplitMulti(mb MultiBuffer, i int) (MultiBuffer, MultiBuffer) {
|
||||||
|
mb2 := make(MultiBuffer, 0, len(mb))
|
||||||
|
if i < len(mb) && i >= 0 {
|
||||||
|
mb2 = append(mb2, mb[i:]...)
|
||||||
|
for j := i; j < len(mb); j++ {
|
||||||
|
mb[j] = nil
|
||||||
|
}
|
||||||
|
mb = mb[:i]
|
||||||
|
}
|
||||||
|
return mb, mb2
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiBuffer writes all buffers from the MultiBuffer to the Writer one by one, and return error if any, with leftover MultiBuffer.
|
||||||
|
func WriteMultiBuffer(writer io.Writer, mb MultiBuffer) (MultiBuffer, error) {
|
||||||
|
for {
|
||||||
|
mb2, b := SplitFirst(mb)
|
||||||
|
mb = mb2
|
||||||
|
if b == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := writer.Write(b.Bytes())
|
||||||
|
b.Release()
|
||||||
|
if err != nil {
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the total number of bytes in the MultiBuffer.
|
||||||
|
func (mb MultiBuffer) Len() int32 {
|
||||||
|
if mb == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int32(0)
|
||||||
|
for _, b := range mb {
|
||||||
|
size += b.Len()
|
||||||
|
}
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the MultiBuffer has no content.
|
||||||
|
func (mb MultiBuffer) IsEmpty() bool {
|
||||||
|
for _, b := range mb {
|
||||||
|
if !b.IsEmpty() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the content of the MultiBuffer in string.
|
||||||
|
func (mb MultiBuffer) String() string {
|
||||||
|
v := make([]interface{}, len(mb))
|
||||||
|
for i, b := range mb {
|
||||||
|
v[i] = b
|
||||||
|
}
|
||||||
|
return serial.Concat(v...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MultiBufferContainer is a ReadWriteCloser wrapper over MultiBuffer.
|
||||||
|
type MultiBufferContainer struct {
|
||||||
|
MultiBuffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader.
|
||||||
|
func (c *MultiBufferContainer) Read(b []byte) (int, error) {
|
||||||
|
if c.MultiBuffer.IsEmpty() {
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
mb, nBytes := SplitBytes(c.MultiBuffer, b)
|
||||||
|
c.MultiBuffer = mb
|
||||||
|
return nBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMultiBuffer implements Reader.
|
||||||
|
func (c *MultiBufferContainer) ReadMultiBuffer() (MultiBuffer, error) {
|
||||||
|
mb := c.MultiBuffer
|
||||||
|
c.MultiBuffer = nil
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (c *MultiBufferContainer) Write(b []byte) (int, error) {
|
||||||
|
c.MultiBuffer = MergeBytes(c.MultiBuffer, b)
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiBuffer implements Writer.
|
||||||
|
func (c *MultiBufferContainer) WriteMultiBuffer(b MultiBuffer) error {
|
||||||
|
mb, _ := MergeMulti(c.MultiBuffer, b)
|
||||||
|
c.MultiBuffer = mb
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closer.
|
||||||
|
func (c *MultiBufferContainer) Close() error {
|
||||||
|
c.MultiBuffer = ReleaseMulti(c.MultiBuffer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
38
common/xray/buf/override.go
Normal file
38
common/xray/buf/override.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/common/xray/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EndpointOverrideReader struct {
|
||||||
|
Reader
|
||||||
|
Dest net.Address
|
||||||
|
OriginalDest net.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *EndpointOverrideReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||||
|
mb, err := r.Reader.ReadMultiBuffer()
|
||||||
|
if err == nil {
|
||||||
|
for _, b := range mb {
|
||||||
|
if b.UDP != nil && b.UDP.Address == r.OriginalDest {
|
||||||
|
b.UDP.Address = r.Dest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mb, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type EndpointOverrideWriter struct {
|
||||||
|
Writer
|
||||||
|
Dest net.Address
|
||||||
|
OriginalDest net.Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *EndpointOverrideWriter) WriteMultiBuffer(mb MultiBuffer) error {
|
||||||
|
for _, b := range mb {
|
||||||
|
if b.UDP != nil && b.UDP.Address == w.Dest {
|
||||||
|
b.UDP.Address = w.OriginalDest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return w.Writer.WriteMultiBuffer(mb)
|
||||||
|
}
|
||||||
175
common/xray/buf/reader.go
Normal file
175
common/xray/buf/reader.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/errors"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func readOneUDP(r io.Reader) (*Buffer, error) {
|
||||||
|
b := New()
|
||||||
|
for i := 0; i < 64; i++ {
|
||||||
|
_, err := b.ReadFrom(r)
|
||||||
|
if !b.IsEmpty() {
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
b.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.Release()
|
||||||
|
return nil, E.New("Reader returns too many empty payloads.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadBuffer reads a Buffer from the given reader.
|
||||||
|
func ReadBuffer(r io.Reader) (*Buffer, error) {
|
||||||
|
b := New()
|
||||||
|
n, err := b.ReadFrom(r)
|
||||||
|
if n > 0 {
|
||||||
|
return b, err
|
||||||
|
}
|
||||||
|
b.Release()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedReader is a Reader that keeps its internal buffer.
|
||||||
|
type BufferedReader struct {
|
||||||
|
// Reader is the underlying reader to be read from
|
||||||
|
Reader Reader
|
||||||
|
// Buffer is the internal buffer to be read from first
|
||||||
|
Buffer MultiBuffer
|
||||||
|
// Splitter is a function to read bytes from MultiBuffer
|
||||||
|
Splitter func(MultiBuffer, []byte) (MultiBuffer, int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedBytes returns the number of bytes that is cached in this reader.
|
||||||
|
func (r *BufferedReader) BufferedBytes() int32 {
|
||||||
|
return r.Buffer.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadByte implements io.ByteReader.
|
||||||
|
func (r *BufferedReader) ReadByte() (byte, error) {
|
||||||
|
var b [1]byte
|
||||||
|
_, err := r.Read(b[:])
|
||||||
|
return b[0], err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read implements io.Reader. It reads from internal buffer first (if available) and then reads from the underlying reader.
|
||||||
|
func (r *BufferedReader) Read(b []byte) (int, error) {
|
||||||
|
spliter := r.Splitter
|
||||||
|
if spliter == nil {
|
||||||
|
spliter = SplitBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
if !r.Buffer.IsEmpty() {
|
||||||
|
buffer, nBytes := spliter(r.Buffer, b)
|
||||||
|
r.Buffer = buffer
|
||||||
|
if r.Buffer.IsEmpty() {
|
||||||
|
r.Buffer = nil
|
||||||
|
}
|
||||||
|
return nBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mb, err := r.Reader.ReadMultiBuffer()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mb, nBytes := spliter(mb, b)
|
||||||
|
if !mb.IsEmpty() {
|
||||||
|
r.Buffer = mb
|
||||||
|
}
|
||||||
|
return nBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMultiBuffer implements Reader.
|
||||||
|
func (r *BufferedReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||||
|
if !r.Buffer.IsEmpty() {
|
||||||
|
mb := r.Buffer
|
||||||
|
r.Buffer = nil
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Reader.ReadMultiBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadAtMost returns a MultiBuffer with at most size.
|
||||||
|
func (r *BufferedReader) ReadAtMost(size int32) (MultiBuffer, error) {
|
||||||
|
if r.Buffer.IsEmpty() {
|
||||||
|
mb, err := r.Reader.ReadMultiBuffer()
|
||||||
|
if mb.IsEmpty() && err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
r.Buffer = mb
|
||||||
|
}
|
||||||
|
|
||||||
|
rb, mb := SplitSize(r.Buffer, size)
|
||||||
|
r.Buffer = rb
|
||||||
|
if r.Buffer.IsEmpty() {
|
||||||
|
r.Buffer = nil
|
||||||
|
}
|
||||||
|
return mb, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *BufferedReader) writeToInternal(writer io.Writer) (int64, error) {
|
||||||
|
mbWriter := NewWriter(writer)
|
||||||
|
var sc SizeCounter
|
||||||
|
if r.Buffer != nil {
|
||||||
|
sc.Size = int64(r.Buffer.Len())
|
||||||
|
if err := mbWriter.WriteMultiBuffer(r.Buffer); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
r.Buffer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
err := Copy(r.Reader, mbWriter, CountSize(&sc))
|
||||||
|
return sc.Size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTo implements io.WriterTo.
|
||||||
|
func (r *BufferedReader) WriteTo(writer io.Writer) (int64, error) {
|
||||||
|
nBytes, err := r.writeToInternal(writer)
|
||||||
|
if errors.Cause(err) == io.EOF {
|
||||||
|
return nBytes, nil
|
||||||
|
}
|
||||||
|
return nBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt implements common.Interruptible.
|
||||||
|
func (r *BufferedReader) Interrupt() {
|
||||||
|
common.Interrupt(r.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closer.
|
||||||
|
func (r *BufferedReader) Close() error {
|
||||||
|
return common.Close(r.Reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SingleReader is a Reader that read one Buffer every time.
|
||||||
|
type SingleReader struct {
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMultiBuffer implements Reader.
|
||||||
|
func (r *SingleReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||||
|
b, err := ReadBuffer(r.Reader)
|
||||||
|
return MultiBuffer{b}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// PacketReader is a Reader that read one Buffer every time.
|
||||||
|
type PacketReader struct {
|
||||||
|
io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMultiBuffer implements Reader.
|
||||||
|
func (r *PacketReader) ReadMultiBuffer() (MultiBuffer, error) {
|
||||||
|
b, err := readOneUDP(r.Reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return MultiBuffer{b}, nil
|
||||||
|
}
|
||||||
270
common/xray/buf/writer.go
Normal file
270
common/xray/buf/writer.go
Normal file
@@ -0,0 +1,270 @@
|
|||||||
|
package buf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/errors"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BufferToBytesWriter is a Writer that writes alloc.Buffer into underlying writer.
|
||||||
|
type BufferToBytesWriter struct {
|
||||||
|
io.Writer
|
||||||
|
|
||||||
|
counter stats.Counter
|
||||||
|
cache [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiBuffer implements Writer. This method takes ownership of the given buffer.
|
||||||
|
func (w *BufferToBytesWriter) WriteMultiBuffer(mb MultiBuffer) error {
|
||||||
|
defer ReleaseMulti(mb)
|
||||||
|
|
||||||
|
size := mb.Len()
|
||||||
|
if size == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(mb) == 1 {
|
||||||
|
return WriteAllBytes(w.Writer, mb[0].Bytes(), w.counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cap(w.cache) < len(mb) {
|
||||||
|
w.cache = make([][]byte, 0, len(mb))
|
||||||
|
}
|
||||||
|
|
||||||
|
bs := w.cache
|
||||||
|
for _, b := range mb {
|
||||||
|
bs = append(bs, b.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
for idx := range bs {
|
||||||
|
bs[idx] = nil
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
nb := net.Buffers(bs)
|
||||||
|
wc := int64(0)
|
||||||
|
defer func() {
|
||||||
|
if w.counter != nil {
|
||||||
|
w.counter.Add(wc)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
for size > 0 {
|
||||||
|
n, err := nb.WriteTo(w.Writer)
|
||||||
|
wc += n
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
size -= int32(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom implements io.ReaderFrom.
|
||||||
|
func (w *BufferToBytesWriter) ReadFrom(reader io.Reader) (int64, error) {
|
||||||
|
var sc SizeCounter
|
||||||
|
err := Copy(NewReader(reader), w, CountSize(&sc))
|
||||||
|
return sc.Size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BufferedWriter is a Writer with internal buffer.
|
||||||
|
type BufferedWriter struct {
|
||||||
|
sync.Mutex
|
||||||
|
writer Writer
|
||||||
|
buffer *Buffer
|
||||||
|
buffered bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBufferedWriter creates a new BufferedWriter.
|
||||||
|
func NewBufferedWriter(writer Writer) *BufferedWriter {
|
||||||
|
return &BufferedWriter{
|
||||||
|
writer: writer,
|
||||||
|
buffer: New(),
|
||||||
|
buffered: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteByte implements io.ByteWriter.
|
||||||
|
func (w *BufferedWriter) WriteByte(c byte) error {
|
||||||
|
return common.Error2(w.Write([]byte{c}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write implements io.Writer.
|
||||||
|
func (w *BufferedWriter) Write(b []byte) (int, error) {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
|
||||||
|
if !w.buffered {
|
||||||
|
if writer, ok := w.writer.(io.Writer); ok {
|
||||||
|
return writer.Write(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
totalBytes := 0
|
||||||
|
for len(b) > 0 {
|
||||||
|
if w.buffer == nil {
|
||||||
|
w.buffer = New()
|
||||||
|
}
|
||||||
|
|
||||||
|
nBytes, err := w.buffer.Write(b)
|
||||||
|
totalBytes += nBytes
|
||||||
|
if err != nil {
|
||||||
|
return totalBytes, err
|
||||||
|
}
|
||||||
|
if !w.buffered || w.buffer.IsFull() {
|
||||||
|
if err := w.flushInternal(); err != nil {
|
||||||
|
return totalBytes, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b = b[nBytes:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalBytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiBuffer implements Writer. It takes ownership of the given MultiBuffer.
|
||||||
|
func (w *BufferedWriter) WriteMultiBuffer(b MultiBuffer) error {
|
||||||
|
if b.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
|
||||||
|
if !w.buffered {
|
||||||
|
return w.writer.WriteMultiBuffer(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
reader := MultiBufferContainer{
|
||||||
|
MultiBuffer: b,
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
|
||||||
|
for !reader.MultiBuffer.IsEmpty() {
|
||||||
|
if w.buffer == nil {
|
||||||
|
w.buffer = New()
|
||||||
|
}
|
||||||
|
common.Must2(w.buffer.ReadFrom(&reader))
|
||||||
|
if w.buffer.IsFull() {
|
||||||
|
if err := w.flushInternal(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush flushes buffered content into underlying writer.
|
||||||
|
func (w *BufferedWriter) Flush() error {
|
||||||
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
|
||||||
|
return w.flushInternal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *BufferedWriter) flushInternal() error {
|
||||||
|
if w.buffer.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := w.buffer
|
||||||
|
w.buffer = nil
|
||||||
|
|
||||||
|
if writer, ok := w.writer.(io.Writer); ok {
|
||||||
|
err := WriteAllBytes(writer, b.Bytes(), nil)
|
||||||
|
b.Release()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return w.writer.WriteMultiBuffer(MultiBuffer{b})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetBuffered sets whether the internal buffer is used. If set to false, Flush() will be called to clear the buffer.
|
||||||
|
func (w *BufferedWriter) SetBuffered(f bool) error {
|
||||||
|
w.Lock()
|
||||||
|
defer w.Unlock()
|
||||||
|
|
||||||
|
w.buffered = f
|
||||||
|
if !f {
|
||||||
|
return w.flushInternal()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadFrom implements io.ReaderFrom.
|
||||||
|
func (w *BufferedWriter) ReadFrom(reader io.Reader) (int64, error) {
|
||||||
|
if err := w.SetBuffered(false); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sc SizeCounter
|
||||||
|
err := Copy(NewReader(reader), w, CountSize(&sc))
|
||||||
|
return sc.Size, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closable.
|
||||||
|
func (w *BufferedWriter) Close() error {
|
||||||
|
if err := w.Flush(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return common.Close(w.writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SequentialWriter is a Writer that writes MultiBuffer sequentially into the underlying io.Writer.
|
||||||
|
type SequentialWriter struct {
|
||||||
|
io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiBuffer implements Writer.
|
||||||
|
func (w *SequentialWriter) WriteMultiBuffer(mb MultiBuffer) error {
|
||||||
|
mb, err := WriteMultiBuffer(w.Writer, mb)
|
||||||
|
ReleaseMulti(mb)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type noOpWriter byte
|
||||||
|
|
||||||
|
func (noOpWriter) WriteMultiBuffer(b MultiBuffer) error {
|
||||||
|
ReleaseMulti(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (noOpWriter) Write(b []byte) (int, error) {
|
||||||
|
return len(b), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (noOpWriter) ReadFrom(reader io.Reader) (int64, error) {
|
||||||
|
b := New()
|
||||||
|
defer b.Release()
|
||||||
|
|
||||||
|
totalBytes := int64(0)
|
||||||
|
for {
|
||||||
|
b.Clear()
|
||||||
|
_, err := b.ReadFrom(reader)
|
||||||
|
totalBytes += int64(b.Len())
|
||||||
|
if err != nil {
|
||||||
|
if errors.Cause(err) == io.EOF {
|
||||||
|
return totalBytes, nil
|
||||||
|
}
|
||||||
|
return totalBytes, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Discard is a Writer that swallows all contents written in.
|
||||||
|
Discard Writer = noOpWriter(0)
|
||||||
|
|
||||||
|
// DiscardBytes is an io.Writer that swallows all contents written in.
|
||||||
|
DiscardBytes io.Writer = noOpWriter(0)
|
||||||
|
)
|
||||||
72
common/xray/bytespool/pool.go
Normal file
72
common/xray/bytespool/pool.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package bytespool
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
func createAllocFunc(size int32) func() interface{} {
|
||||||
|
return func() interface{} {
|
||||||
|
return make([]byte, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following parameters controls the size of buffer pools.
|
||||||
|
// There are numPools pools. Starting from 2k size, the size of each pool is sizeMulti of the previous one.
|
||||||
|
// Package buf is guaranteed to not use buffers larger than the largest pool.
|
||||||
|
// Other packets may use larger buffers.
|
||||||
|
const (
|
||||||
|
numPools = 4
|
||||||
|
sizeMulti = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
pool [numPools]sync.Pool
|
||||||
|
poolSize [numPools]int32
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
size := int32(2048)
|
||||||
|
for i := 0; i < numPools; i++ {
|
||||||
|
pool[i] = sync.Pool{
|
||||||
|
New: createAllocFunc(size),
|
||||||
|
}
|
||||||
|
poolSize[i] = size
|
||||||
|
size *= sizeMulti
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPool returns a sync.Pool that generates bytes array with at least the given size.
|
||||||
|
// It may return nil if no such pool exists.
|
||||||
|
//
|
||||||
|
// xray:api:stable
|
||||||
|
func GetPool(size int32) *sync.Pool {
|
||||||
|
for idx, ps := range poolSize {
|
||||||
|
if size <= ps {
|
||||||
|
return &pool[idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alloc returns a byte slice with at least the given size. Minimum size of returned slice is 2048.
|
||||||
|
//
|
||||||
|
// xray:api:stable
|
||||||
|
func Alloc(size int32) []byte {
|
||||||
|
pool := GetPool(size)
|
||||||
|
if pool != nil {
|
||||||
|
return pool.Get().([]byte)
|
||||||
|
}
|
||||||
|
return make([]byte, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Free puts a byte slice into the internal pool.
|
||||||
|
//
|
||||||
|
// xray:api:stable
|
||||||
|
func Free(b []byte) {
|
||||||
|
size := int32(cap(b))
|
||||||
|
b = b[0:cap(b)]
|
||||||
|
for i := numPools - 1; i >= 0; i-- {
|
||||||
|
if size >= poolSize[i] {
|
||||||
|
pool[i].Put(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
19
common/xray/common.go
Normal file
19
common/xray/common.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// Must panics if err is not nil.
|
||||||
|
func Must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Must2 panics if the second parameter is not nil, otherwise returns the first parameter.
|
||||||
|
func Must2(v interface{}, err error) interface{} {
|
||||||
|
Must(err)
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error2 returns the err from the 2nd parameter.
|
||||||
|
func Error2(v interface{}, err error) error {
|
||||||
|
return err
|
||||||
|
}
|
||||||
14
common/xray/crypto/crypto.go
Normal file
14
common/xray/crypto/crypto.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package crypto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RandBetween(from int64, to int64) int64 {
|
||||||
|
if from == to {
|
||||||
|
return from
|
||||||
|
}
|
||||||
|
bigInt, _ := rand.Int(rand.Reader, big.NewInt(to-from))
|
||||||
|
return from + bigInt.Int64()
|
||||||
|
}
|
||||||
25
common/xray/errors/errors.go
Normal file
25
common/xray/errors/errors.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
type hasInnerError interface {
|
||||||
|
// Unwrap returns the underlying error of this one.
|
||||||
|
Unwrap() error
|
||||||
|
}
|
||||||
|
|
||||||
|
func Cause(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
L:
|
||||||
|
for {
|
||||||
|
switch inner := err.(type) {
|
||||||
|
case hasInnerError:
|
||||||
|
if inner.Unwrap() == nil {
|
||||||
|
break L
|
||||||
|
}
|
||||||
|
err = inner.Unwrap()
|
||||||
|
default:
|
||||||
|
break L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
52
common/xray/interfaces.go
Normal file
52
common/xray/interfaces.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// Closable is the interface for objects that can release its resources.
|
||||||
|
//
|
||||||
|
// xray:api:beta
|
||||||
|
type Closable interface {
|
||||||
|
// Close release all resources used by this object, including goroutines.
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interruptible is an interface for objects that can be stopped before its completion.
|
||||||
|
//
|
||||||
|
// xray:api:beta
|
||||||
|
type Interruptible interface {
|
||||||
|
Interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close closes the obj if it is a Closable.
|
||||||
|
//
|
||||||
|
// xray:api:beta
|
||||||
|
func Close(obj interface{}) error {
|
||||||
|
if c, ok := obj.(Closable); ok {
|
||||||
|
return c.Close()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt calls Interrupt() if object implements Interruptible interface, or Close() if the object implements Closable interface.
|
||||||
|
//
|
||||||
|
// xray:api:beta
|
||||||
|
func Interrupt(obj interface{}) error {
|
||||||
|
if c, ok := obj.(Interruptible); ok {
|
||||||
|
c.Interrupt()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return Close(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runnable is the interface for objects that can start to work and stop on demand.
|
||||||
|
type Runnable interface {
|
||||||
|
// Start starts the runnable object. Upon the method returning nil, the object begins to function properly.
|
||||||
|
Start() error
|
||||||
|
|
||||||
|
Closable
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasType is the interface for objects that knows its type.
|
||||||
|
type HasType interface {
|
||||||
|
// Type returns the type of the object.
|
||||||
|
// Usually it returns (*Type)(nil) of the object.
|
||||||
|
Type() interface{}
|
||||||
|
}
|
||||||
62
common/xray/json/badoption/range.go
Normal file
62
common/xray/json/badoption/range.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package badoption
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/crypto"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Range struct {
|
||||||
|
From int32 `json:"from"`
|
||||||
|
To int32 `json:"to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Range) Build() *Range {
|
||||||
|
return (*Range)(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Range) MarshalJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(fmt.Sprintf("%d-%d", c.From, c.To))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Range) UnmarshalJSON(content []byte) error {
|
||||||
|
var rangeValue struct {
|
||||||
|
From int32 `json:"from"`
|
||||||
|
To int32 `json:"to"`
|
||||||
|
}
|
||||||
|
var stringValue string
|
||||||
|
err := json.Unmarshal(content, &stringValue)
|
||||||
|
if err == nil {
|
||||||
|
parts := strings.Split(stringValue, "-")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return E.New("invalid length of range parts")
|
||||||
|
}
|
||||||
|
from, err := strconv.ParseInt(parts[0], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
to, err := strconv.ParseInt(parts[1], 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rangeValue.From, rangeValue.To = int32(from), int32(to)
|
||||||
|
} else {
|
||||||
|
err := json.Unmarshal(content, &rangeValue)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rangeValue.From > rangeValue.To {
|
||||||
|
return E.New("invalid range")
|
||||||
|
}
|
||||||
|
*c = Range{rangeValue.From, rangeValue.To}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Range) Rand() int32 {
|
||||||
|
return int32(crypto.RandBetween(int64(c.From), int64(c.To)))
|
||||||
|
}
|
||||||
181
common/xray/net/address.go
Normal file
181
common/xray/net/address.go
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// LocalHostIP is a constant value for localhost IP in IPv4.
|
||||||
|
LocalHostIP = IPAddress([]byte{127, 0, 0, 1})
|
||||||
|
|
||||||
|
// AnyIP is a constant value for any IP in IPv4.
|
||||||
|
AnyIP = IPAddress([]byte{0, 0, 0, 0})
|
||||||
|
|
||||||
|
// LocalHostDomain is a constant value for localhost domain.
|
||||||
|
LocalHostDomain = DomainAddress("localhost")
|
||||||
|
|
||||||
|
// LocalHostIPv6 is a constant value for localhost IP in IPv6.
|
||||||
|
LocalHostIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})
|
||||||
|
|
||||||
|
// AnyIPv6 is a constant value for any IP in IPv6.
|
||||||
|
AnyIPv6 = IPAddress([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddressFamily is the type of address.
|
||||||
|
type AddressFamily byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AddressFamilyIPv4 represents address as IPv4
|
||||||
|
AddressFamilyIPv4 = AddressFamily(0)
|
||||||
|
|
||||||
|
// AddressFamilyIPv6 represents address as IPv6
|
||||||
|
AddressFamilyIPv6 = AddressFamily(1)
|
||||||
|
|
||||||
|
// AddressFamilyDomain represents address as Domain
|
||||||
|
AddressFamilyDomain = AddressFamily(2)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsIPv4 returns true if current AddressFamily is IPv4.
|
||||||
|
func (af AddressFamily) IsIPv4() bool {
|
||||||
|
return af == AddressFamilyIPv4
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIPv6 returns true if current AddressFamily is IPv6.
|
||||||
|
func (af AddressFamily) IsIPv6() bool {
|
||||||
|
return af == AddressFamilyIPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsIP returns true if current AddressFamily is IPv6 or IPv4.
|
||||||
|
func (af AddressFamily) IsIP() bool {
|
||||||
|
return af == AddressFamilyIPv4 || af == AddressFamilyIPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDomain returns true if current AddressFamily is Domain.
|
||||||
|
func (af AddressFamily) IsDomain() bool {
|
||||||
|
return af == AddressFamilyDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address represents a network address to be communicated with. It may be an IP address or domain
|
||||||
|
// address, not both. This interface doesn't resolve IP address for a given domain.
|
||||||
|
type Address interface {
|
||||||
|
IP() net.IP // IP of this Address
|
||||||
|
Domain() string // Domain of this Address
|
||||||
|
Family() AddressFamily
|
||||||
|
|
||||||
|
String() string // String representation of this Address
|
||||||
|
}
|
||||||
|
|
||||||
|
func isAlphaNum(c byte) bool {
|
||||||
|
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseAddress parses a string into an Address. The return value will be an IPAddress when
|
||||||
|
// the string is in the form of IPv4 or IPv6 address, or a DomainAddress otherwise.
|
||||||
|
func ParseAddress(addr string) Address {
|
||||||
|
// Handle IPv6 address in form as "[2001:4860:0:2001::68]"
|
||||||
|
lenAddr := len(addr)
|
||||||
|
if lenAddr > 0 && addr[0] == '[' && addr[lenAddr-1] == ']' {
|
||||||
|
addr = addr[1 : lenAddr-1]
|
||||||
|
lenAddr -= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
if lenAddr > 0 && (!isAlphaNum(addr[0]) || !isAlphaNum(addr[len(addr)-1])) {
|
||||||
|
addr = strings.TrimSpace(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
ip := net.ParseIP(addr)
|
||||||
|
if ip != nil {
|
||||||
|
return IPAddress(ip)
|
||||||
|
}
|
||||||
|
return DomainAddress(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes0 = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
|
||||||
|
// IPAddress creates an Address with given IP.
|
||||||
|
func IPAddress(ip []byte) Address {
|
||||||
|
switch len(ip) {
|
||||||
|
case net.IPv4len:
|
||||||
|
var addr ipv4Address = [4]byte{ip[0], ip[1], ip[2], ip[3]}
|
||||||
|
return addr
|
||||||
|
case net.IPv6len:
|
||||||
|
if bytes.Equal(ip[:10], bytes0) && ip[10] == 0xff && ip[11] == 0xff {
|
||||||
|
return IPAddress(ip[12:16])
|
||||||
|
}
|
||||||
|
var addr ipv6Address = [16]byte{
|
||||||
|
ip[0], ip[1], ip[2], ip[3],
|
||||||
|
ip[4], ip[5], ip[6], ip[7],
|
||||||
|
ip[8], ip[9], ip[10], ip[11],
|
||||||
|
ip[12], ip[13], ip[14], ip[15],
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DomainAddress creates an Address with given domain.
|
||||||
|
// This is an internal function that forcibly converts a string to domain.
|
||||||
|
// It's mainly used in test files and mux.
|
||||||
|
// Unless you have a specific reason, use net.ParseAddress instead,
|
||||||
|
// as this function does not check whether the input is an IP address.
|
||||||
|
// Otherwise, you will get strange results like domain: 1.1.1.1
|
||||||
|
func DomainAddress(domain string) Address {
|
||||||
|
return domainAddress(domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipv4Address [4]byte
|
||||||
|
|
||||||
|
func (a ipv4Address) IP() net.IP {
|
||||||
|
return net.IP(a[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ipv4Address) Domain() string {
|
||||||
|
panic("Calling Domain() on an IPv4Address.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ipv4Address) Family() AddressFamily {
|
||||||
|
return AddressFamilyIPv4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ipv4Address) String() string {
|
||||||
|
return a.IP().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ipv6Address [16]byte
|
||||||
|
|
||||||
|
func (a ipv6Address) IP() net.IP {
|
||||||
|
return net.IP(a[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ipv6Address) Domain() string {
|
||||||
|
panic("Calling Domain() on an IPv6Address.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ipv6Address) Family() AddressFamily {
|
||||||
|
return AddressFamilyIPv6
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a ipv6Address) String() string {
|
||||||
|
return "[" + a.IP().String() + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
type domainAddress string
|
||||||
|
|
||||||
|
func (domainAddress) IP() net.IP {
|
||||||
|
panic("Calling IP() on a DomainAddress.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a domainAddress) Domain() string {
|
||||||
|
return string(a)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (domainAddress) Family() AddressFamily {
|
||||||
|
return AddressFamilyDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a domainAddress) String() string {
|
||||||
|
return a.Domain()
|
||||||
|
}
|
||||||
146
common/xray/net/destination.go
Normal file
146
common/xray/net/destination.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Destination represents a network destination including address and protocol (tcp / udp).
|
||||||
|
type Destination struct {
|
||||||
|
Address Address
|
||||||
|
Port Port
|
||||||
|
Network Network
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestinationFromAddr generates a Destination from a net address.
|
||||||
|
func DestinationFromAddr(addr net.Addr) Destination {
|
||||||
|
switch addr := addr.(type) {
|
||||||
|
case *net.TCPAddr:
|
||||||
|
return TCPDestination(IPAddress(addr.IP), Port(addr.Port))
|
||||||
|
case *net.UDPAddr:
|
||||||
|
return UDPDestination(IPAddress(addr.IP), Port(addr.Port))
|
||||||
|
case *net.UnixAddr:
|
||||||
|
return UnixDestination(DomainAddress(addr.Name))
|
||||||
|
default:
|
||||||
|
panic("Net: Unknown address type.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDestination converts a destination from its string presentation.
|
||||||
|
func ParseDestination(dest string) (Destination, error) {
|
||||||
|
d := Destination{
|
||||||
|
Address: AnyIP,
|
||||||
|
Port: Port(0),
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(dest, "tcp:") {
|
||||||
|
d.Network = Network_TCP
|
||||||
|
dest = dest[4:]
|
||||||
|
} else if strings.HasPrefix(dest, "udp:") {
|
||||||
|
d.Network = Network_UDP
|
||||||
|
dest = dest[4:]
|
||||||
|
} else if strings.HasPrefix(dest, "unix:") {
|
||||||
|
d = UnixDestination(DomainAddress(dest[5:]))
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hstr, pstr, err := SplitHostPort(dest)
|
||||||
|
if err != nil {
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
if len(hstr) > 0 {
|
||||||
|
d.Address = ParseAddress(hstr)
|
||||||
|
}
|
||||||
|
if len(pstr) > 0 {
|
||||||
|
port, err := PortFromString(pstr)
|
||||||
|
if err != nil {
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
d.Port = port
|
||||||
|
}
|
||||||
|
return d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TCPDestination creates a TCP destination with given address
|
||||||
|
func TCPDestination(address Address, port Port) Destination {
|
||||||
|
return Destination{
|
||||||
|
Network: Network_TCP,
|
||||||
|
Address: address,
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UDPDestination creates a UDP destination with given address
|
||||||
|
func UDPDestination(address Address, port Port) Destination {
|
||||||
|
return Destination{
|
||||||
|
Network: Network_UDP,
|
||||||
|
Address: address,
|
||||||
|
Port: port,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnixDestination creates a Unix destination with given address
|
||||||
|
func UnixDestination(address Address) Destination {
|
||||||
|
return Destination{
|
||||||
|
Network: Network_UNIX,
|
||||||
|
Address: address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NetAddr returns the network address in this Destination in string form.
|
||||||
|
func (d Destination) NetAddr() string {
|
||||||
|
addr := ""
|
||||||
|
if d.Network == Network_TCP || d.Network == Network_UDP {
|
||||||
|
addr = d.Address.String() + ":" + d.Port.String()
|
||||||
|
} else if d.Network == Network_UNIX {
|
||||||
|
addr = d.Address.String()
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RawNetAddr converts a net.Addr from its Destination presentation.
|
||||||
|
func (d Destination) RawNetAddr() net.Addr {
|
||||||
|
var addr net.Addr
|
||||||
|
switch d.Network {
|
||||||
|
case Network_TCP:
|
||||||
|
if d.Address.Family().IsIP() {
|
||||||
|
addr = &net.TCPAddr{
|
||||||
|
IP: d.Address.IP(),
|
||||||
|
Port: int(d.Port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Network_UDP:
|
||||||
|
if d.Address.Family().IsIP() {
|
||||||
|
addr = &net.UDPAddr{
|
||||||
|
IP: d.Address.IP(),
|
||||||
|
Port: int(d.Port),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Network_UNIX:
|
||||||
|
if d.Address.Family().IsDomain() {
|
||||||
|
addr = &net.UnixAddr{
|
||||||
|
Name: d.Address.String(),
|
||||||
|
Net: d.Network.SystemString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the strings form of this Destination.
|
||||||
|
func (d Destination) String() string {
|
||||||
|
prefix := "unknown:"
|
||||||
|
switch d.Network {
|
||||||
|
case Network_TCP:
|
||||||
|
prefix = "tcp:"
|
||||||
|
case Network_UDP:
|
||||||
|
prefix = "udp:"
|
||||||
|
case Network_UNIX:
|
||||||
|
prefix = "unix:"
|
||||||
|
}
|
||||||
|
return prefix + d.NetAddr()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if this Destination is valid.
|
||||||
|
func (d Destination) IsValid() bool {
|
||||||
|
return d.Network != Network_Unknown
|
||||||
|
}
|
||||||
13
common/xray/net/net.go
Normal file
13
common/xray/net/net.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// defines the maximum time an idle TCP session can survive in the tunnel, so
|
||||||
|
// it should be consistent across HTTP versions and with other transports.
|
||||||
|
const ConnIdleTimeout = 300 * time.Second
|
||||||
|
|
||||||
|
// consistent with quic-go
|
||||||
|
const QuicgoH3KeepAlivePeriod = 10 * time.Second
|
||||||
|
|
||||||
|
// consistent with chrome
|
||||||
|
const ChromeH2KeepAlivePeriod = 45 * time.Second
|
||||||
33
common/xray/net/network.go
Normal file
33
common/xray/net/network.go
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
type Network int32
|
||||||
|
|
||||||
|
const (
|
||||||
|
Network_Unknown Network = 0
|
||||||
|
Network_TCP Network = 2
|
||||||
|
Network_UDP Network = 3
|
||||||
|
Network_UNIX Network = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
func (n Network) SystemString() string {
|
||||||
|
switch n {
|
||||||
|
case Network_TCP:
|
||||||
|
return "tcp"
|
||||||
|
case Network_UDP:
|
||||||
|
return "udp"
|
||||||
|
case Network_UNIX:
|
||||||
|
return "unix"
|
||||||
|
default:
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasNetwork returns true if the network list has a certain network.
|
||||||
|
func HasNetwork(list []Network, network Network) bool {
|
||||||
|
for _, value := range list {
|
||||||
|
if value == network {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
55
common/xray/net/port.go
Normal file
55
common/xray/net/port.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Port represents a network port in TCP and UDP protocol.
|
||||||
|
type Port uint16
|
||||||
|
|
||||||
|
// PortFromBytes converts a byte array to a Port, assuming bytes are in big endian order.
|
||||||
|
// @unsafe Caller must ensure that the byte array has at least 2 elements.
|
||||||
|
func PortFromBytes(port []byte) Port {
|
||||||
|
return Port(binary.BigEndian.Uint16(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PortFromInt converts an integer to a Port.
|
||||||
|
// @error when the integer is not positive or larger then 65535
|
||||||
|
func PortFromInt(val uint32) (Port, error) {
|
||||||
|
if val > 65535 {
|
||||||
|
return Port(0), E.New("invalid port range: ", val)
|
||||||
|
}
|
||||||
|
return Port(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PortFromString converts a string to a Port.
|
||||||
|
// @error when the string is not an integer or the integral value is a not a valid Port.
|
||||||
|
func PortFromString(s string) (Port, error) {
|
||||||
|
val, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return Port(0), E.New("invalid port range: ", s)
|
||||||
|
}
|
||||||
|
return PortFromInt(uint32(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value return the corresponding uint16 value of a Port.
|
||||||
|
func (p Port) Value() uint16 {
|
||||||
|
return uint16(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string presentation of a Port.
|
||||||
|
func (p Port) String() string {
|
||||||
|
return strconv.Itoa(int(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemoryPortRange struct {
|
||||||
|
From Port
|
||||||
|
To Port
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MemoryPortRange) Contains(port Port) bool {
|
||||||
|
return r.From <= port && port <= r.To
|
||||||
|
}
|
||||||
84
common/xray/net/system.go
Normal file
84
common/xray/net/system.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package net
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
// DialTCP is an alias of net.DialTCP.
|
||||||
|
var (
|
||||||
|
DialTCP = net.DialTCP
|
||||||
|
DialUDP = net.DialUDP
|
||||||
|
DialUnix = net.DialUnix
|
||||||
|
Dial = net.Dial
|
||||||
|
)
|
||||||
|
|
||||||
|
type ListenConfig = net.ListenConfig
|
||||||
|
|
||||||
|
var (
|
||||||
|
Listen = net.Listen
|
||||||
|
ListenTCP = net.ListenTCP
|
||||||
|
ListenUDP = net.ListenUDP
|
||||||
|
ListenUnix = net.ListenUnix
|
||||||
|
)
|
||||||
|
|
||||||
|
var LookupIP = net.LookupIP
|
||||||
|
|
||||||
|
var FileConn = net.FileConn
|
||||||
|
|
||||||
|
// ParseIP is an alias of net.ParseIP
|
||||||
|
var ParseIP = net.ParseIP
|
||||||
|
|
||||||
|
var SplitHostPort = net.SplitHostPort
|
||||||
|
|
||||||
|
var CIDRMask = net.CIDRMask
|
||||||
|
|
||||||
|
type (
|
||||||
|
Addr = net.Addr
|
||||||
|
Conn = net.Conn
|
||||||
|
PacketConn = net.PacketConn
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
TCPAddr = net.TCPAddr
|
||||||
|
TCPConn = net.TCPConn
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
UDPAddr = net.UDPAddr
|
||||||
|
UDPConn = net.UDPConn
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
UnixAddr = net.UnixAddr
|
||||||
|
UnixConn = net.UnixConn
|
||||||
|
)
|
||||||
|
|
||||||
|
// IP is an alias for net.IP.
|
||||||
|
type (
|
||||||
|
IP = net.IP
|
||||||
|
IPMask = net.IPMask
|
||||||
|
IPNet = net.IPNet
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
IPv4len = net.IPv4len
|
||||||
|
IPv6len = net.IPv6len
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Error = net.Error
|
||||||
|
AddrError = net.AddrError
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Dialer = net.Dialer
|
||||||
|
Listener = net.Listener
|
||||||
|
TCPListener = net.TCPListener
|
||||||
|
UnixListener = net.UnixListener
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ResolveTCPAddr = net.ResolveTCPAddr
|
||||||
|
ResolveUDPAddr = net.ResolveUDPAddr
|
||||||
|
ResolveUnixAddr = net.ResolveUnixAddr
|
||||||
|
)
|
||||||
|
|
||||||
|
type Resolver = net.Resolver
|
||||||
215
common/xray/pipe/impl.go
Normal file
215
common/xray/pipe/impl.go
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
package pipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/buf"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
open state = iota
|
||||||
|
closed
|
||||||
|
errord
|
||||||
|
)
|
||||||
|
|
||||||
|
type pipeOption struct {
|
||||||
|
limit int32 // maximum buffer size in bytes
|
||||||
|
discardOverflow bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *pipeOption) isFull(curSize int32) bool {
|
||||||
|
return o.limit >= 0 && curSize > o.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
type pipe struct {
|
||||||
|
sync.Mutex
|
||||||
|
data buf.MultiBuffer
|
||||||
|
readSignal *signal.Notifier
|
||||||
|
writeSignal *signal.Notifier
|
||||||
|
done *done.Instance
|
||||||
|
errChan chan error
|
||||||
|
option pipeOption
|
||||||
|
state state
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
errBufferFull = errors.New("buffer full")
|
||||||
|
errSlowDown = errors.New("slow down")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *pipe) Len() int32 {
|
||||||
|
data := p.data
|
||||||
|
if data == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return data.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) getState(forRead bool) error {
|
||||||
|
switch p.state {
|
||||||
|
case open:
|
||||||
|
if !forRead && p.option.isFull(p.data.Len()) {
|
||||||
|
return errBufferFull
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case closed:
|
||||||
|
if !forRead {
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
if !p.data.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return io.EOF
|
||||||
|
case errord:
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
default:
|
||||||
|
panic("impossible case")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) readMultiBufferInternal() (buf.MultiBuffer, error) {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if err := p.getState(true); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data := p.data
|
||||||
|
p.data = nil
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||||
|
for {
|
||||||
|
data, err := p.readMultiBufferInternal()
|
||||||
|
if data != nil || err != nil {
|
||||||
|
p.writeSignal.Signal()
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.readSignal.Wait():
|
||||||
|
case <-p.done.Wait():
|
||||||
|
case err = <-p.errChan:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
|
||||||
|
timer := time.NewTimer(d)
|
||||||
|
defer timer.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
data, err := p.readMultiBufferInternal()
|
||||||
|
if data != nil || err != nil {
|
||||||
|
p.writeSignal.Signal()
|
||||||
|
return data, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.readSignal.Wait():
|
||||||
|
case <-p.done.Wait():
|
||||||
|
case <-timer.C:
|
||||||
|
return nil, buf.ErrReadTimeout
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) writeMultiBufferInternal(mb buf.MultiBuffer) error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if err := p.getState(false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.data == nil {
|
||||||
|
p.data = mb
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.data, _ = buf.MergeMulti(p.data, mb)
|
||||||
|
return errSlowDown
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
|
if mb.IsEmpty() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
err := p.writeMultiBufferInternal(mb)
|
||||||
|
if err == nil {
|
||||||
|
p.readSignal.Signal()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == errSlowDown {
|
||||||
|
p.readSignal.Signal()
|
||||||
|
|
||||||
|
// Yield current goroutine. Hopefully the reading counterpart can pick up the payload.
|
||||||
|
runtime.Gosched()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == errBufferFull && p.option.discardOverflow {
|
||||||
|
buf.ReleaseMulti(mb)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != errBufferFull {
|
||||||
|
buf.ReleaseMulti(mb)
|
||||||
|
p.readSignal.Signal()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-p.writeSignal.Wait():
|
||||||
|
case <-p.done.Wait():
|
||||||
|
return io.ErrClosedPipe
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *pipe) Close() error {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if p.state == closed || p.state == errord {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p.state = closed
|
||||||
|
common.Must(p.done.Close())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt implements common.Interruptible.
|
||||||
|
func (p *pipe) Interrupt() {
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if p.state == closed || p.state == errord {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
p.state = errord
|
||||||
|
|
||||||
|
if !p.data.IsEmpty() {
|
||||||
|
buf.ReleaseMulti(p.data)
|
||||||
|
p.data = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
common.Must(p.done.Close())
|
||||||
|
}
|
||||||
53
common/xray/pipe/pipe.go
Normal file
53
common/xray/pipe/pipe.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package pipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option for creating new Pipes.
|
||||||
|
type Option func(*pipeOption)
|
||||||
|
|
||||||
|
// WithoutSizeLimit returns an Option for Pipe to have no size limit.
|
||||||
|
func WithoutSizeLimit() Option {
|
||||||
|
return func(opt *pipeOption) {
|
||||||
|
opt.limit = -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSizeLimit returns an Option for Pipe to have the given size limit.
|
||||||
|
func WithSizeLimit(limit int32) Option {
|
||||||
|
return func(opt *pipeOption) {
|
||||||
|
opt.limit = limit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DiscardOverflow returns an Option for Pipe to discard writes if full.
|
||||||
|
func DiscardOverflow() Option {
|
||||||
|
return func(opt *pipeOption) {
|
||||||
|
opt.discardOverflow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Reader and Writer that connects to each other.
|
||||||
|
func New(opts ...Option) (*Reader, *Writer) {
|
||||||
|
p := &pipe{
|
||||||
|
readSignal: signal.NewNotifier(),
|
||||||
|
writeSignal: signal.NewNotifier(),
|
||||||
|
done: done.New(),
|
||||||
|
errChan: make(chan error, 1),
|
||||||
|
option: pipeOption{
|
||||||
|
limit: -1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&(p.option))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Reader{
|
||||||
|
pipe: p,
|
||||||
|
}, &Writer{
|
||||||
|
pipe: p,
|
||||||
|
}
|
||||||
|
}
|
||||||
41
common/xray/pipe/reader.go
Normal file
41
common/xray/pipe/reader.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package pipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/buf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reader is a buf.Reader that reads content from a pipe.
|
||||||
|
type Reader struct {
|
||||||
|
pipe *pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMultiBuffer implements buf.Reader.
|
||||||
|
func (r *Reader) ReadMultiBuffer() (buf.MultiBuffer, error) {
|
||||||
|
return r.pipe.ReadMultiBuffer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadMultiBufferTimeout reads content from a pipe within the given duration, or returns buf.ErrTimeout otherwise.
|
||||||
|
func (r *Reader) ReadMultiBufferTimeout(d time.Duration) (buf.MultiBuffer, error) {
|
||||||
|
return r.pipe.ReadMultiBufferTimeout(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt implements common.Interruptible.
|
||||||
|
func (r *Reader) Interrupt() {
|
||||||
|
r.pipe.Interrupt()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReturnAnError makes ReadMultiBuffer return an error, only once.
|
||||||
|
func (r *Reader) ReturnAnError(err error) {
|
||||||
|
r.pipe.errChan <- err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recover catches an error set by ReturnAnError, if exists.
|
||||||
|
func (r *Reader) Recover() (err error) {
|
||||||
|
select {
|
||||||
|
case err = <-r.pipe.errChan:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
29
common/xray/pipe/writer.go
Normal file
29
common/xray/pipe/writer.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package pipe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/common/xray/buf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer is a buf.Writer that writes data into a pipe.
|
||||||
|
type Writer struct {
|
||||||
|
pipe *pipe
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteMultiBuffer implements buf.Writer.
|
||||||
|
func (w *Writer) WriteMultiBuffer(mb buf.MultiBuffer) error {
|
||||||
|
return w.pipe.WriteMultiBuffer(mb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements io.Closer. After the pipe is closed, writing to the pipe will return io.ErrClosedPipe, while reading will return io.EOF.
|
||||||
|
func (w *Writer) Close() error {
|
||||||
|
return w.pipe.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) Len() int32 {
|
||||||
|
return w.pipe.Len()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interrupt implements common.Interruptible.
|
||||||
|
func (w *Writer) Interrupt() {
|
||||||
|
w.pipe.Interrupt()
|
||||||
|
}
|
||||||
29
common/xray/serial/serial.go
Normal file
29
common/xray/serial/serial.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package serial
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadUint16 reads first two bytes from the reader, and then converts them to an uint16 value.
|
||||||
|
func ReadUint16(reader io.Reader) (uint16, error) {
|
||||||
|
var b [2]byte
|
||||||
|
if _, err := io.ReadFull(reader, b[:]); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return binary.BigEndian.Uint16(b[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteUint16 writes an uint16 value into writer.
|
||||||
|
func WriteUint16(writer io.Writer, value uint16) (int, error) {
|
||||||
|
var b [2]byte
|
||||||
|
binary.BigEndian.PutUint16(b[:], value)
|
||||||
|
return writer.Write(b[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteUint64 writes an uint64 value into writer.
|
||||||
|
func WriteUint64(writer io.Writer, value uint64) (int, error) {
|
||||||
|
var b [8]byte
|
||||||
|
binary.BigEndian.PutUint64(b[:], value)
|
||||||
|
return writer.Write(b[:])
|
||||||
|
}
|
||||||
35
common/xray/serial/string.go
Normal file
35
common/xray/serial/string.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package serial
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToString serializes an arbitrary value into string.
|
||||||
|
func ToString(v interface{}) string {
|
||||||
|
if v == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
switch value := v.(type) {
|
||||||
|
case string:
|
||||||
|
return value
|
||||||
|
case *string:
|
||||||
|
return *value
|
||||||
|
case fmt.Stringer:
|
||||||
|
return value.String()
|
||||||
|
case error:
|
||||||
|
return value.Error()
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%+v", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concat concatenates all input into a single string.
|
||||||
|
func Concat(v ...interface{}) string {
|
||||||
|
builder := strings.Builder{}
|
||||||
|
for _, value := range v {
|
||||||
|
builder.WriteString(ToString(value))
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
49
common/xray/signal/done/done.go
Normal file
49
common/xray/signal/done/done.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package done
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Instance is a utility for notifications of something being done.
|
||||||
|
type Instance struct {
|
||||||
|
access sync.Mutex
|
||||||
|
c chan struct{}
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Done.
|
||||||
|
func New() *Instance {
|
||||||
|
return &Instance{
|
||||||
|
c: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Done returns true if Close() is called.
|
||||||
|
func (d *Instance) Done() bool {
|
||||||
|
select {
|
||||||
|
case <-d.Wait():
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait returns a channel for waiting for done.
|
||||||
|
func (d *Instance) Wait() <-chan struct{} {
|
||||||
|
return d.c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close marks this Done 'done'. This method may be called multiple times. All calls after first call will have no effect on its status.
|
||||||
|
func (d *Instance) Close() error {
|
||||||
|
d.access.Lock()
|
||||||
|
defer d.access.Unlock()
|
||||||
|
|
||||||
|
if d.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.closed = true
|
||||||
|
close(d.c)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
26
common/xray/signal/notifier.go
Normal file
26
common/xray/signal/notifier.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package signal
|
||||||
|
|
||||||
|
// Notifier is a utility for notifying changes. The change producer may notify changes multiple time, and the consumer may get notified asynchronously.
|
||||||
|
type Notifier struct {
|
||||||
|
c chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNotifier creates a new Notifier.
|
||||||
|
func NewNotifier() *Notifier {
|
||||||
|
return &Notifier{
|
||||||
|
c: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal signals a change, usually by producer. This method never blocks.
|
||||||
|
func (n *Notifier) Signal() {
|
||||||
|
select {
|
||||||
|
case n.c <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait returns a channel for waiting for changes. The returned channel never gets closed.
|
||||||
|
func (n *Notifier) Wait() <-chan struct{} {
|
||||||
|
return n.c
|
||||||
|
}
|
||||||
105
common/xray/signal/pubsub/pubsub.go
Normal file
105
common/xray/signal/pubsub/pubsub.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package pubsub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal/done"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Subscriber struct {
|
||||||
|
buffer chan interface{}
|
||||||
|
done *done.Instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriber) push(msg interface{}) {
|
||||||
|
select {
|
||||||
|
case s.buffer <- msg:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriber) Wait() <-chan interface{} {
|
||||||
|
return s.buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriber) Close() error {
|
||||||
|
return s.done.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Subscriber) IsClosed() bool {
|
||||||
|
return s.done.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
sync.RWMutex
|
||||||
|
subs map[string][]*Subscriber
|
||||||
|
ctask *task.Periodic
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService() *Service {
|
||||||
|
s := &Service{
|
||||||
|
subs: make(map[string][]*Subscriber),
|
||||||
|
}
|
||||||
|
s.ctask = &task.Periodic{
|
||||||
|
Execute: s.Cleanup,
|
||||||
|
Interval: time.Second * 30,
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup cleans up internal caches of subscribers.
|
||||||
|
// Visible for testing only.
|
||||||
|
func (s *Service) Cleanup() error {
|
||||||
|
s.Lock()
|
||||||
|
defer s.Unlock()
|
||||||
|
|
||||||
|
if len(s.subs) == 0 {
|
||||||
|
return errors.New("nothing to do")
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, subs := range s.subs {
|
||||||
|
newSub := make([]*Subscriber, 0, len(s.subs))
|
||||||
|
for _, sub := range subs {
|
||||||
|
if !sub.IsClosed() {
|
||||||
|
newSub = append(newSub, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(newSub) == 0 {
|
||||||
|
delete(s.subs, name)
|
||||||
|
} else {
|
||||||
|
s.subs[name] = newSub
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(s.subs) == 0 {
|
||||||
|
s.subs = make(map[string][]*Subscriber)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Subscribe(name string) *Subscriber {
|
||||||
|
sub := &Subscriber{
|
||||||
|
buffer: make(chan interface{}, 16),
|
||||||
|
done: done.New(),
|
||||||
|
}
|
||||||
|
s.Lock()
|
||||||
|
s.subs[name] = append(s.subs[name], sub)
|
||||||
|
s.Unlock()
|
||||||
|
common.Must(s.ctask.Start())
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Publish(name string, message interface{}) {
|
||||||
|
s.RLock()
|
||||||
|
defer s.RUnlock()
|
||||||
|
|
||||||
|
for _, sub := range s.subs[name] {
|
||||||
|
if !sub.IsClosed() {
|
||||||
|
sub.push(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
common/xray/signal/semaphore/semaphore.go
Normal file
27
common/xray/signal/semaphore/semaphore.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package semaphore
|
||||||
|
|
||||||
|
// Instance is an implementation of semaphore.
|
||||||
|
type Instance struct {
|
||||||
|
token chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New create a new Semaphore with n permits.
|
||||||
|
func New(n int) *Instance {
|
||||||
|
s := &Instance{
|
||||||
|
token: make(chan struct{}, n),
|
||||||
|
}
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
s.token <- struct{}{}
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait returns a channel for acquiring a permit.
|
||||||
|
func (s *Instance) Wait() <-chan struct{} {
|
||||||
|
return s.token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signal releases a permit into the semaphore.
|
||||||
|
func (s *Instance) Signal() {
|
||||||
|
s.token <- struct{}{}
|
||||||
|
}
|
||||||
82
common/xray/signal/timer.go
Normal file
82
common/xray/signal/timer.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package signal
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
"github.com/sagernet/sing-box/common/xray/task"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ActivityUpdater interface {
|
||||||
|
Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ActivityTimer struct {
|
||||||
|
sync.RWMutex
|
||||||
|
updated chan struct{}
|
||||||
|
checkTask *task.Periodic
|
||||||
|
onTimeout func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ActivityTimer) Update() {
|
||||||
|
select {
|
||||||
|
case t.updated <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ActivityTimer) check() error {
|
||||||
|
select {
|
||||||
|
case <-t.updated:
|
||||||
|
default:
|
||||||
|
t.finish()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ActivityTimer) finish() {
|
||||||
|
t.Lock()
|
||||||
|
defer t.Unlock()
|
||||||
|
|
||||||
|
if t.onTimeout != nil {
|
||||||
|
t.onTimeout()
|
||||||
|
t.onTimeout = nil
|
||||||
|
}
|
||||||
|
if t.checkTask != nil {
|
||||||
|
t.checkTask.Close()
|
||||||
|
t.checkTask = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *ActivityTimer) SetTimeout(timeout time.Duration) {
|
||||||
|
if timeout == 0 {
|
||||||
|
t.finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
checkTask := &task.Periodic{
|
||||||
|
Interval: timeout,
|
||||||
|
Execute: t.check,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Lock()
|
||||||
|
|
||||||
|
if t.checkTask != nil {
|
||||||
|
t.checkTask.Close()
|
||||||
|
}
|
||||||
|
t.checkTask = checkTask
|
||||||
|
t.Unlock()
|
||||||
|
t.Update()
|
||||||
|
common.Must(checkTask.Start())
|
||||||
|
}
|
||||||
|
|
||||||
|
func CancelAfterInactivity(ctx context.Context, cancel context.CancelFunc, timeout time.Duration) *ActivityTimer {
|
||||||
|
timer := &ActivityTimer{
|
||||||
|
updated: make(chan struct{}, 1),
|
||||||
|
onTimeout: cancel,
|
||||||
|
}
|
||||||
|
timer.SetTimeout(timeout)
|
||||||
|
return timer
|
||||||
|
}
|
||||||
34
common/xray/stat/connection.go
Normal file
34
common/xray/stat/connection.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
package stat
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/stats"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Connection interface {
|
||||||
|
net.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type CounterConnection struct {
|
||||||
|
Connection
|
||||||
|
ReadCounter stats.Counter
|
||||||
|
WriteCounter stats.Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CounterConnection) Read(b []byte) (int, error) {
|
||||||
|
nBytes, err := c.Connection.Read(b)
|
||||||
|
if c.ReadCounter != nil {
|
||||||
|
c.ReadCounter.Add(int64(nBytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nBytes, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CounterConnection) Write(b []byte) (int, error) {
|
||||||
|
nBytes, err := c.Connection.Write(b)
|
||||||
|
if c.WriteCounter != nil {
|
||||||
|
c.WriteCounter.Add(int64(nBytes))
|
||||||
|
}
|
||||||
|
return nBytes, err
|
||||||
|
}
|
||||||
13
common/xray/stats/stats.go
Normal file
13
common/xray/stats/stats.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package stats
|
||||||
|
|
||||||
|
// Counter is the interface for stats counters.
|
||||||
|
//
|
||||||
|
// xray:api:stable
|
||||||
|
type Counter interface {
|
||||||
|
// Value is the current value of the counter.
|
||||||
|
Value() int64
|
||||||
|
// Set sets a new value to the counter, and returns the previous one.
|
||||||
|
Set(int64) int64
|
||||||
|
// Add adds a value to the current counter value, and returns the previous value.
|
||||||
|
Add(int64) int64
|
||||||
|
}
|
||||||
10
common/xray/task/common.go
Normal file
10
common/xray/task/common.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing-box/common/xray"
|
||||||
|
|
||||||
|
// Close returns a func() that closes v.
|
||||||
|
func Close(v interface{}) func() error {
|
||||||
|
return func() error {
|
||||||
|
return common.Close(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
85
common/xray/task/periodic.go
Normal file
85
common/xray/task/periodic.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Periodic is a task that runs periodically.
|
||||||
|
type Periodic struct {
|
||||||
|
// Interval of the task being run
|
||||||
|
Interval time.Duration
|
||||||
|
// Execute is the task function
|
||||||
|
Execute func() error
|
||||||
|
|
||||||
|
access sync.Mutex
|
||||||
|
timer *time.Timer
|
||||||
|
running bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Periodic) hasClosed() bool {
|
||||||
|
t.access.Lock()
|
||||||
|
defer t.access.Unlock()
|
||||||
|
|
||||||
|
return !t.running
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Periodic) checkedExecute() error {
|
||||||
|
if t.hasClosed() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.Execute(); err != nil {
|
||||||
|
t.access.Lock()
|
||||||
|
t.running = false
|
||||||
|
t.access.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.access.Lock()
|
||||||
|
defer t.access.Unlock()
|
||||||
|
|
||||||
|
if !t.running {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.timer = time.AfterFunc(t.Interval, func() {
|
||||||
|
t.checkedExecute()
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start implements common.Runnable.
|
||||||
|
func (t *Periodic) Start() error {
|
||||||
|
t.access.Lock()
|
||||||
|
if t.running {
|
||||||
|
t.access.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t.running = true
|
||||||
|
t.access.Unlock()
|
||||||
|
|
||||||
|
if err := t.checkedExecute(); err != nil {
|
||||||
|
t.access.Lock()
|
||||||
|
t.running = false
|
||||||
|
t.access.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements common.Closable.
|
||||||
|
func (t *Periodic) Close() error {
|
||||||
|
t.access.Lock()
|
||||||
|
defer t.access.Unlock()
|
||||||
|
|
||||||
|
t.running = false
|
||||||
|
if t.timer != nil {
|
||||||
|
t.timer.Stop()
|
||||||
|
t.timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
64
common/xray/task/task.go
Normal file
64
common/xray/task/task.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package task
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray/signal/semaphore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OnSuccess executes g() after f() returns nil.
|
||||||
|
func OnSuccess(f func() error, g func() error) func() error {
|
||||||
|
return func() error {
|
||||||
|
if err := f(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return g()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run executes a list of tasks in parallel, returns the first error encountered or nil if all tasks pass.
|
||||||
|
func Run(ctx context.Context, tasks ...func() error) error {
|
||||||
|
n := len(tasks)
|
||||||
|
s := semaphore.New(n)
|
||||||
|
done := make(chan error, 1)
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
<-s.Wait()
|
||||||
|
go func(f func() error) {
|
||||||
|
err := f()
|
||||||
|
if err == nil {
|
||||||
|
s.Signal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case done <- err:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}(task)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if altctx := ctx.Value("altctx"); altctx != nil {
|
||||||
|
ctx = altctx.(context.Context)
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
select {
|
||||||
|
case err := <-done:
|
||||||
|
return err
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-s.Wait():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
if cancel := ctx.Value("cancel"); cancel != nil {
|
||||||
|
cancel.(context.CancelFunc)()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
101
common/xray/uuid/uuid.go
Normal file
101
common/xray/uuid/uuid.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package uuid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/xray"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var byteGroups = []int{8, 4, 4, 4, 12}
|
||||||
|
|
||||||
|
type UUID [16]byte
|
||||||
|
|
||||||
|
// String returns the string representation of this UUID.
|
||||||
|
func (u *UUID) String() string {
|
||||||
|
bytes := u.Bytes()
|
||||||
|
result := hex.EncodeToString(bytes[0 : byteGroups[0]/2])
|
||||||
|
start := byteGroups[0] / 2
|
||||||
|
for i := 1; i < len(byteGroups); i++ {
|
||||||
|
nBytes := byteGroups[i] / 2
|
||||||
|
result += "-"
|
||||||
|
result += hex.EncodeToString(bytes[start : start+nBytes])
|
||||||
|
start += nBytes
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bytes returns the bytes representation of this UUID.
|
||||||
|
func (u *UUID) Bytes() []byte {
|
||||||
|
return u[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns true if this UUID equals another UUID by value.
|
||||||
|
func (u *UUID) Equals(another *UUID) bool {
|
||||||
|
if u == nil && another == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if u == nil || another == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return bytes.Equal(u.Bytes(), another.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a UUID with random value.
|
||||||
|
func New() UUID {
|
||||||
|
var uuid UUID
|
||||||
|
common.Must2(rand.Read(uuid.Bytes()))
|
||||||
|
uuid[6] = (uuid[6] & 0x0f) | (4 << 4)
|
||||||
|
uuid[8] = (uuid[8]&(0xff>>2) | (0x02 << 6))
|
||||||
|
return uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytes converts a UUID in byte form to object.
|
||||||
|
func ParseBytes(b []byte) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
if len(b) != 16 {
|
||||||
|
return uuid, E.New("invalid UUID: ", b)
|
||||||
|
}
|
||||||
|
copy(uuid[:], b)
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseString converts a UUID in string form to object.
|
||||||
|
func ParseString(str string) (UUID, error) {
|
||||||
|
var uuid UUID
|
||||||
|
|
||||||
|
text := []byte(str)
|
||||||
|
if l := len(text); l < 32 || l > 36 {
|
||||||
|
if l == 0 || l > 30 {
|
||||||
|
return uuid, E.New("invalid UUID: ", str)
|
||||||
|
}
|
||||||
|
h := sha1.New()
|
||||||
|
h.Write(uuid[:])
|
||||||
|
h.Write(text)
|
||||||
|
u := h.Sum(nil)[:16]
|
||||||
|
u[6] = (u[6] & 0x0f) | (5 << 4)
|
||||||
|
u[8] = (u[8]&(0xff>>2) | (0x02 << 6))
|
||||||
|
copy(uuid[:], u)
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
b := uuid.Bytes()
|
||||||
|
|
||||||
|
for _, byteGroup := range byteGroups {
|
||||||
|
if text[0] == '-' {
|
||||||
|
text = text[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := hex.Decode(b[:byteGroup/2], text[:byteGroup]); err != nil {
|
||||||
|
return uuid, err
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text[byteGroup:]
|
||||||
|
b = b[byteGroup/2:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return uuid, nil
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ const (
|
|||||||
DNSTypeFakeIP = "fakeip"
|
DNSTypeFakeIP = "fakeip"
|
||||||
DNSTypeDHCP = "dhcp"
|
DNSTypeDHCP = "dhcp"
|
||||||
DNSTypeTailscale = "tailscale"
|
DNSTypeTailscale = "tailscale"
|
||||||
|
DNSTypeSDNS = "sdns"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ const (
|
|||||||
TypeTrojan = "trojan"
|
TypeTrojan = "trojan"
|
||||||
TypeNaive = "naive"
|
TypeNaive = "naive"
|
||||||
TypeWireGuard = "wireguard"
|
TypeWireGuard = "wireguard"
|
||||||
|
TypeWARP = "warp"
|
||||||
TypeHysteria = "hysteria"
|
TypeHysteria = "hysteria"
|
||||||
TypeTor = "tor"
|
TypeTor = "tor"
|
||||||
TypeSSH = "ssh"
|
TypeSSH = "ssh"
|
||||||
TypeShadowTLS = "shadowtls"
|
TypeShadowTLS = "shadowtls"
|
||||||
|
TypeMieru = "mieru"
|
||||||
TypeAnyTLS = "anytls"
|
TypeAnyTLS = "anytls"
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeVLESS = "vless"
|
TypeVLESS = "vless"
|
||||||
TypeTUIC = "tuic"
|
TypeTUIC = "tuic"
|
||||||
TypeHysteria2 = "hysteria2"
|
TypeHysteria2 = "hysteria2"
|
||||||
|
TypeTunnelClient = "tunnel_client"
|
||||||
|
TypeTunnelServer = "tunnel_server"
|
||||||
TypeTailscale = "tailscale"
|
TypeTailscale = "tailscale"
|
||||||
TypeDERP = "derp"
|
TypeDERP = "derp"
|
||||||
TypeResolved = "resolved"
|
TypeResolved = "resolved"
|
||||||
@@ -65,6 +69,8 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "Naive"
|
return "Naive"
|
||||||
case TypeWireGuard:
|
case TypeWireGuard:
|
||||||
return "WireGuard"
|
return "WireGuard"
|
||||||
|
case TypeWARP:
|
||||||
|
return "WARP"
|
||||||
case TypeHysteria:
|
case TypeHysteria:
|
||||||
return "Hysteria"
|
return "Hysteria"
|
||||||
case TypeTor:
|
case TypeTor:
|
||||||
@@ -81,12 +87,18 @@ func ProxyDisplayName(proxyType string) string {
|
|||||||
return "TUIC"
|
return "TUIC"
|
||||||
case TypeHysteria2:
|
case TypeHysteria2:
|
||||||
return "Hysteria2"
|
return "Hysteria2"
|
||||||
|
case TypeMieru:
|
||||||
|
return "Mieru"
|
||||||
case TypeAnyTLS:
|
case TypeAnyTLS:
|
||||||
return "AnyTLS"
|
return "AnyTLS"
|
||||||
case TypeSelector:
|
case TypeSelector:
|
||||||
return "Selector"
|
return "Selector"
|
||||||
case TypeURLTest:
|
case TypeURLTest:
|
||||||
return "URLTest"
|
return "URLTest"
|
||||||
|
case TypeTunnelClient:
|
||||||
|
return "Tunnel Client"
|
||||||
|
case TypeTunnelServer:
|
||||||
|
return "Tunnel Server"
|
||||||
default:
|
default:
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ const (
|
|||||||
V2RayTransportTypeQUIC = "quic"
|
V2RayTransportTypeQUIC = "quic"
|
||||||
V2RayTransportTypeGRPC = "grpc"
|
V2RayTransportTypeGRPC = "grpc"
|
||||||
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
V2RayTransportTypeHTTPUpgrade = "httpupgrade"
|
||||||
|
V2RayTransportTypeXHTTP = "xhttp"
|
||||||
)
|
)
|
||||||
|
|||||||
20
constant/warp.go
Normal file
20
constant/warp.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package constant
|
||||||
|
|
||||||
|
type WARPConfig struct {
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
Interface struct {
|
||||||
|
Addresses struct {
|
||||||
|
V4 string `json:"v4"`
|
||||||
|
V6 string `json:"v6"`
|
||||||
|
} `json:"addresses"`
|
||||||
|
} `json:"interface"`
|
||||||
|
Peers []struct {
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
Endpoint struct {
|
||||||
|
V4 string `json:"v4"`
|
||||||
|
V6 string `json:"v6"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Ports []int `json:"ports"`
|
||||||
|
} `json:"endpoint"`
|
||||||
|
} `json:"peers"`
|
||||||
|
}
|
||||||
71
dns/transport/sdns.go
Normal file
71
dns/transport/sdns.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package transport
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/ameshkov/dnscrypt/v2"
|
||||||
|
mDNS "github.com/miekg/dns"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.DNSTransport = (*SDNSTransport)(nil)
|
||||||
|
|
||||||
|
func RegisterSDNS(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.SDNSDNSServerOptions](registry, C.DNSTypeSDNS, NewSDNSTransport)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SDNSTransport struct {
|
||||||
|
dns.TransportAdapter
|
||||||
|
client *dnscrypt.Client
|
||||||
|
name string
|
||||||
|
stamp string
|
||||||
|
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSDNSTransport(ctx context.Context, logger log.ContextLogger, tag string, options option.SDNSDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
|
transportDialer, err := dns.NewRemoteDialer(ctx, options.RemoteDNSServerOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &SDNSTransport{
|
||||||
|
client: &dnscrypt.Client{
|
||||||
|
Net: "udp",
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return transportDialer.DialContext(ctx, N.NetworkName(network), M.ParseSocksaddr(addr))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
stamp: options.Stamp,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SDNSTransport) Name() string {
|
||||||
|
return t.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SDNSTransport) Start(adapter.StartStage) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SDNSTransport) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *SDNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
|
||||||
|
resolverInfo, err := t.client.Dial(t.stamp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return t.client.Exchange(message, resolverInfo)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||||
| `tuic` | [TUIC](./tuic/) |
|
| `tuic` | [TUIC](./tuic/) |
|
||||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||||
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `anytls` | [AnyTLS](./anytls/) |
|
| `anytls` | [AnyTLS](./anytls/) |
|
||||||
| `tor` | [Tor](./tor/) |
|
| `tor` | [Tor](./tor/) |
|
||||||
| `ssh` | [SSH](./ssh/) |
|
| `ssh` | [SSH](./ssh/) |
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||||
| `tuic` | [TUIC](./tuic/) |
|
| `tuic` | [TUIC](./tuic/) |
|
||||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||||
|
| `mieru` | [Mieru](./mieru/) |
|
||||||
| `anytls` | [AnyTLS](./anytls/) |
|
| `anytls` | [AnyTLS](./anytls/) |
|
||||||
| `tor` | [Tor](./tor/) |
|
| `tor` | [Tor](./tor/) |
|
||||||
| `ssh` | [SSH](./ssh/) |
|
| `ssh` | [SSH](./ssh/) |
|
||||||
|
|||||||
71
docs/configuration/outbound/mieru.md
Normal file
71
docs/configuration/outbound/mieru.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
### Structure
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mieru",
|
||||||
|
"tag": "mieru-out",
|
||||||
|
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"server_ports": [
|
||||||
|
"9000-9010",
|
||||||
|
"9020-9030"
|
||||||
|
],
|
||||||
|
"transport": "TCP",
|
||||||
|
"username": "asdf",
|
||||||
|
"password": "hjkl",
|
||||||
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
|
|
||||||
|
... // Dial Fields
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fields
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
The server address.
|
||||||
|
|
||||||
|
#### server_port
|
||||||
|
|
||||||
|
The server port.
|
||||||
|
|
||||||
|
Must set at least one field between `server_port` and `server_ports`.
|
||||||
|
|
||||||
|
#### server_ports
|
||||||
|
|
||||||
|
Server port range list.
|
||||||
|
|
||||||
|
Must set at least one field between `server_port` and `server_ports`.
|
||||||
|
|
||||||
|
#### transport
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
Transmission protocol. The only allowed value is `TCP`.
|
||||||
|
|
||||||
|
#### username
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
mieru user name.
|
||||||
|
|
||||||
|
#### password
|
||||||
|
|
||||||
|
==Required==
|
||||||
|
|
||||||
|
mieru password.
|
||||||
|
|
||||||
|
#### multiplexing
|
||||||
|
|
||||||
|
Multiplexing level. Supported values are `MULTIPLEXING_OFF`, `MULTIPLEXING_LOW`, `MULTIPLEXING_MIDDLE`, `MULTIPLEXING_HIGH`. `MULTIPLEXING_OFF` disables multiplexing.
|
||||||
|
|
||||||
|
### Dial Fields
|
||||||
|
|
||||||
|
See [Dial Fields](/configuration/shared/dial/) for details.
|
||||||
71
docs/configuration/outbound/mieru.zh.md
Normal file
71
docs/configuration/outbound/mieru.zh.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
icon: material/new-box
|
||||||
|
---
|
||||||
|
|
||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mieru",
|
||||||
|
"tag": "mieru-out",
|
||||||
|
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 1080,
|
||||||
|
"server_ports": [
|
||||||
|
"9000-9010",
|
||||||
|
"9020-9030"
|
||||||
|
],
|
||||||
|
"transport": "TCP",
|
||||||
|
"username": "asdf",
|
||||||
|
"password": "hjkl",
|
||||||
|
"multiplexing": "MULTIPLEXING_LOW",
|
||||||
|
|
||||||
|
... // 拨号字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### server
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
服务器地址。
|
||||||
|
|
||||||
|
#### server_port
|
||||||
|
|
||||||
|
服务器端口。
|
||||||
|
|
||||||
|
必须填写 `server_port` 和 `server_ports` 中至少一项。
|
||||||
|
|
||||||
|
#### server_ports
|
||||||
|
|
||||||
|
服务器端口范围列表。
|
||||||
|
|
||||||
|
必须填写 `server_port` 和 `server_ports` 中至少一项。
|
||||||
|
|
||||||
|
#### transport
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
通信协议。仅可设为 `TCP`。
|
||||||
|
|
||||||
|
#### username
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
mieru 用户名。
|
||||||
|
|
||||||
|
#### password
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
mieru 密码。
|
||||||
|
|
||||||
|
#### multiplexing
|
||||||
|
|
||||||
|
多路复用设置。可以设为 `MULTIPLEXING_OFF`,`MULTIPLEXING_LOW`,`MULTIPLEXING_MIDDLE`,`MULTIPLEXING_HIGH`。其中 `MULTIPLEXING_OFF` 会关闭多路复用功能。
|
||||||
|
|
||||||
|
### 拨号字段
|
||||||
|
|
||||||
|
参阅 [拨号字段](/zh/configuration/shared/dial/)。
|
||||||
58
examples/amnezia/client.json
Normal file
58
examples/amnezia/client.json
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "wireguard",
|
||||||
|
"tag": "wireguard-out",
|
||||||
|
"mtu": 1408,
|
||||||
|
"address": null,
|
||||||
|
"private_key": "",
|
||||||
|
"listen_port": 10000,
|
||||||
|
"peers": [
|
||||||
|
{
|
||||||
|
"address": "example.com",
|
||||||
|
"port": 10001,
|
||||||
|
"reserved": "AAAA"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"udp_timeout": "5m0s",
|
||||||
|
"amnezia": {
|
||||||
|
"jc": 120,
|
||||||
|
"jmin": 23,
|
||||||
|
"jmax": 911,
|
||||||
|
"h1": 1,
|
||||||
|
"h2": 2,
|
||||||
|
"h3": 3,
|
||||||
|
"h4": 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "wireguard-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
43
examples/mieru/client.json
Normal file
43
examples/mieru/client.json
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "mieru",
|
||||||
|
"tag": "mieru-out",
|
||||||
|
"server": "example.com",
|
||||||
|
"server_port": 27017,
|
||||||
|
"server_ports": "27017-27019",
|
||||||
|
"transport": "TCP",
|
||||||
|
"username": "username",
|
||||||
|
"password": "password",
|
||||||
|
"multiplexing": "MULTIPLEXING_LOW"
|
||||||
|
// Dial Fields
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "mieru-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
32
examples/sdns/client.json
Normal file
32
examples/sdns/client.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "sdns",
|
||||||
|
"stamp": "sdns://AQMAAAAAAAAAETk0LjE0MC4xNS4xNTo1NDQzINErR_JS3PLCu_iZEIbq95zkSV2LFsigxDIuUso_OQhzIjIuZG5zY3J5cHQuZGVmYXVsdC5uczEuYWRndWFyZC5jb20",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
57
examples/tunnel/client->server/client.json
Normal file
57
examples/tunnel/client->server/client.json
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_client",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||||
|
"outbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": 8000,
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"tag": "dns-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"outbound": "tunnel",
|
||||||
|
"override_tunnel_destination": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
49
examples/tunnel/client->server/server.json
Normal file
49
examples/tunnel/client->server/server.json
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_server",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-in",
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"listen_port": 8000,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "vless",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
52
examples/tunnel/client1->server->client2/client1.json
Normal file
52
examples/tunnel/client1->server->client2/client1.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_client",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||||
|
"outbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": 8000,
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"outbound": "tunnel",
|
||||||
|
"override_tunnel_destination": "487f6073-3300-4819-a07d-39652e45fb4d"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final": "direct-out",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
40
examples/tunnel/client1->server->client2/client2.json
Normal file
40
examples/tunnel/client1->server->client2/client2.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_client",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
|
||||||
|
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f",
|
||||||
|
"outbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": 8000,
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
66
examples/tunnel/client1->server->client2/server.json
Normal file
66
examples/tunnel/client1->server->client2/server.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_server",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "487f6073-3300-4819-a07d-39652e45fb4d",
|
||||||
|
"key": "3d74d616-2502-4c17-9cc3-92c366550f4f"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-in",
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"listen_port": 8000,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "vless",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"tunnel_source": [
|
||||||
|
"9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"487f6073-3300-4819-a07d-39652e45fb4d"
|
||||||
|
],
|
||||||
|
"tunnel_destination": [
|
||||||
|
"9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"487f6073-3300-4819-a07d-39652e45fb4d"
|
||||||
|
],
|
||||||
|
"outbound": "tunnel"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": 8000,
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"network": "tcp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"tag": "dns-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "vless-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_server",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-in",
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"listen_port": 8000,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "vless",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"inbound": "vless-in",
|
||||||
|
"outbound": "tunnel",
|
||||||
|
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_client",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||||
|
"outbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": 8000,
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
40
examples/tunnel/server->client/client.json
Normal file
40
examples/tunnel/server->client/client.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_client",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe",
|
||||||
|
"outbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "0.0.0.0",
|
||||||
|
"server_port": 8000,
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"network": "tcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
62
examples/tunnel/server->client/server.json
Normal file
62
examples/tunnel/server->client/server.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "tunnel_server",
|
||||||
|
"tag": "tunnel",
|
||||||
|
"uuid": "f79f7678-55e7-432d-a15f-6e8ab2b7fe13",
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937",
|
||||||
|
"key": "1c9b2ccf-b0c0-4c26-868d-a55a4edad3fe"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbound": {
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-in",
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"listen_port": 8000,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "vless",
|
||||||
|
"uuid": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"outbound": "tunnel",
|
||||||
|
"override_tunnel_destination": "9b65b7e1-04c8-4717-8f45-2aa61fd25937"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"final": "direct-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
66
examples/unified_delay/client.json
Normal file
66
examples/unified_delay/client.json
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "warp",
|
||||||
|
"tag": "warp-out",
|
||||||
|
"listen_port": 10000,
|
||||||
|
"udp_timeout": "5m0s",
|
||||||
|
"amnezia": {
|
||||||
|
"jc": 120,
|
||||||
|
"jmin": 23,
|
||||||
|
"jmax": 911,
|
||||||
|
"h1": 1,
|
||||||
|
"h2": 2,
|
||||||
|
"h3": 3,
|
||||||
|
"h4": 4
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"detour": "direct"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "selector",
|
||||||
|
"tag": "select-out",
|
||||||
|
"outbounds": [
|
||||||
|
"direct",
|
||||||
|
"warp-out"
|
||||||
|
],
|
||||||
|
"default": "direct",
|
||||||
|
"interrupt_exist_connections": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"unified_delay": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
62
examples/warp/client.json
Normal file
62
examples/warp/client.json
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"endpoints": [
|
||||||
|
{
|
||||||
|
"type": "warp",
|
||||||
|
"tag": "warp-out",
|
||||||
|
"listen_port": 10000,
|
||||||
|
"udp_timeout": "5m0s",
|
||||||
|
"amnezia": {
|
||||||
|
"jc": 120,
|
||||||
|
"jmin": 23,
|
||||||
|
"jmax": 911,
|
||||||
|
"h1": 1,
|
||||||
|
"h2": 2,
|
||||||
|
"h3": 3,
|
||||||
|
"h4": 4
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"detour": "direct",
|
||||||
|
// for getting existing WARP device profile
|
||||||
|
"id": "",
|
||||||
|
"private_key": "",
|
||||||
|
"auth_token": ""
|
||||||
|
}
|
||||||
|
// Dial Fields
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "warp-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"cache_file": {
|
||||||
|
"enabled": true,
|
||||||
|
"store_warp_config": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
85
examples/xhttp/client.json
Normal file
85
examples/xhttp/client.json
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"tag": "mixed-in",
|
||||||
|
"listen_port": 7897
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"server": "example.com",
|
||||||
|
"server_port": 443,
|
||||||
|
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668",
|
||||||
|
"tls": {
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": "example.com",
|
||||||
|
"alpn": "h2" // h3 for QUIC
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"type": "xhttp",
|
||||||
|
"mode": "stream-up",
|
||||||
|
"host": "example.com",
|
||||||
|
"path": "/xhttp",
|
||||||
|
"domain_strategy": "prefer_ipv4",
|
||||||
|
"xmux": {
|
||||||
|
"max_concurrency": "0-1",
|
||||||
|
"max_connections": "0-1",
|
||||||
|
"c_max_reuse_times": "0-1",
|
||||||
|
"h_max_request_times": "0-1",
|
||||||
|
"h_max_reusable_secs": "0-1",
|
||||||
|
"h_keep_alive_period": 60
|
||||||
|
},
|
||||||
|
"download": {
|
||||||
|
"mode": "",
|
||||||
|
"host": "example.com",
|
||||||
|
"path": "/xhttp",
|
||||||
|
"domain_strategy": "prefer_ipv4",
|
||||||
|
"x_padding_bytes": "0-0",
|
||||||
|
"sc_max_each_post_bytes": "0-0",
|
||||||
|
"sc_min_posts_interval_ms": "0-0",
|
||||||
|
"sc_stream_up_server_secs": "0-0",
|
||||||
|
"xmux": {
|
||||||
|
"max_concurrency": "0-1",
|
||||||
|
"max_connections": "0-1",
|
||||||
|
"c_max_reuse_times": "0-1",
|
||||||
|
"h_max_request_times": "0-1",
|
||||||
|
"h_max_reusable_secs": "0-1",
|
||||||
|
"h_keep_alive_period": 60
|
||||||
|
},
|
||||||
|
"server": "example.com",
|
||||||
|
"server_port": 443,
|
||||||
|
"tls": {
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": "example.com",
|
||||||
|
"alpn": "h2" // h3 for QUIC
|
||||||
|
},
|
||||||
|
"detour": "direct"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"packet_encoding": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "vless-out",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
50
examples/xhttp/server.json
Normal file
50
examples/xhttp/server.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "error"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [
|
||||||
|
{
|
||||||
|
"type": "local",
|
||||||
|
"tag": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "vless",
|
||||||
|
"tag": "vless-out",
|
||||||
|
"listen": "0.0.0.0",
|
||||||
|
"listen_port": 443,
|
||||||
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "user",
|
||||||
|
"uuid": "3179dce2-2ff9-413c-85b4-c1d53ed41668"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tls": {
|
||||||
|
"enabled": true,
|
||||||
|
"server_name": "example.com",
|
||||||
|
"alpn": "h2", // h3 for QUIC
|
||||||
|
"certificate_path": "/path/to/fullchain.pem",
|
||||||
|
"key_path": "/path/to/privkey.pem"
|
||||||
|
},
|
||||||
|
"transport": {
|
||||||
|
"type": "xhttp",
|
||||||
|
"mode": "stream-up",
|
||||||
|
"path": "/xhttp",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"final": "direct",
|
||||||
|
"default_domain_resolver": "default",
|
||||||
|
"auto_detect_interface": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,6 +43,7 @@ type CacheFile struct {
|
|||||||
cacheID []byte
|
cacheID []byte
|
||||||
storeFakeIP bool
|
storeFakeIP bool
|
||||||
storeRDRC bool
|
storeRDRC bool
|
||||||
|
storeWARPConfig bool
|
||||||
rdrcTimeout time.Duration
|
rdrcTimeout time.Duration
|
||||||
DB *bbolt.DB
|
DB *bbolt.DB
|
||||||
saveMetadataTimer *time.Timer
|
saveMetadataTimer *time.Timer
|
||||||
@@ -80,16 +81,17 @@ func New(ctx context.Context, options option.CacheFileOptions) *CacheFile {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &CacheFile{
|
return &CacheFile{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
path: filemanager.BasePath(ctx, path),
|
path: filemanager.BasePath(ctx, path),
|
||||||
cacheID: cacheIDBytes,
|
cacheID: cacheIDBytes,
|
||||||
storeFakeIP: options.StoreFakeIP,
|
storeFakeIP: options.StoreFakeIP,
|
||||||
storeRDRC: options.StoreRDRC,
|
storeRDRC: options.StoreRDRC,
|
||||||
rdrcTimeout: rdrcTimeout,
|
storeWARPConfig: options.StoreWARPConfig,
|
||||||
saveDomain: make(map[netip.Addr]string),
|
rdrcTimeout: rdrcTimeout,
|
||||||
saveAddress4: make(map[string]netip.Addr),
|
saveDomain: make(map[netip.Addr]string),
|
||||||
saveAddress6: make(map[string]netip.Addr),
|
saveAddress4: make(map[string]netip.Addr),
|
||||||
saveRDRC: make(map[saveRDRCCacheKey]bool),
|
saveAddress6: make(map[string]netip.Addr),
|
||||||
|
saveRDRC: make(map[saveRDRCCacheKey]bool),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,3 +318,40 @@ func (c *CacheFile) SaveRuleSet(tag string, set *adapter.SavedBinary) error {
|
|||||||
return bucket.Put([]byte(tag), setBinary)
|
return bucket.Put([]byte(tag), setBinary)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) StoreWARPConfig() bool {
|
||||||
|
return c.storeWARPConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) LoadWARPConfig(tag string) *adapter.SavedBinary {
|
||||||
|
var savedConfig adapter.SavedBinary
|
||||||
|
err := c.DB.View(func(t *bbolt.Tx) error {
|
||||||
|
bucket := c.bucket(t, bucketRuleSet)
|
||||||
|
if bucket == nil {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
configBinary := bucket.Get([]byte(tag))
|
||||||
|
if len(configBinary) == 0 {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
return savedConfig.UnmarshalBinary(configBinary)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &savedConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CacheFile) SaveWARPConfig(tag string, set *adapter.SavedBinary) error {
|
||||||
|
return c.DB.Batch(func(t *bbolt.Tx) error {
|
||||||
|
bucket, err := c.createBucket(t, bucketRuleSet)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configBinary, err := set.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return bucket.Put([]byte(tag), configBinary)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
|
|||||||
cancel()
|
cancel()
|
||||||
return nil, E.Cause(err, "create service")
|
return nil, E.Cause(err, "create service")
|
||||||
}
|
}
|
||||||
|
experimentalOptions := common.PtrValueOrDefault(options.Experimental)
|
||||||
|
if experimentalOptions.UnifiedDelay != nil && experimentalOptions.UnifiedDelay.Enabled {
|
||||||
|
ctx = urltest.ContextWithIsUnifiedDelay(ctx)
|
||||||
|
}
|
||||||
runtimeDebug.FreeOSMemory()
|
runtimeDebug.FreeOSMemory()
|
||||||
return &BoxService{
|
return &BoxService{
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
|
|||||||
33
go.mod
33
go.mod
@@ -1,12 +1,15 @@
|
|||||||
module github.com/sagernet/sing-box
|
module github.com/sagernet/sing-box
|
||||||
|
|
||||||
go 1.23.1
|
go 1.24.1
|
||||||
|
|
||||||
|
toolchain go1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anytls/sing-anytls v0.0.8
|
github.com/anytls/sing-anytls v0.0.8
|
||||||
github.com/caddyserver/certmagic v0.23.0
|
github.com/caddyserver/certmagic v0.23.0
|
||||||
github.com/coder/websocket v1.8.13
|
github.com/coder/websocket v1.8.13
|
||||||
github.com/cretz/bine v0.2.0
|
github.com/cretz/bine v0.2.0
|
||||||
|
github.com/enfein/mieru/v3 v3.17.1
|
||||||
github.com/go-chi/chi/v5 v5.2.2
|
github.com/go-chi/chi/v5 v5.2.2
|
||||||
github.com/go-chi/render v1.0.3
|
github.com/go-chi/render v1.0.3
|
||||||
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
|
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
|
||||||
@@ -20,6 +23,7 @@ require (
|
|||||||
github.com/mholt/acmez/v3 v3.1.2
|
github.com/mholt/acmez/v3 v3.1.2
|
||||||
github.com/miekg/dns v1.1.67
|
github.com/miekg/dns v1.1.67
|
||||||
github.com/oschwald/maxminddb-golang v1.13.1
|
github.com/oschwald/maxminddb-golang v1.13.1
|
||||||
|
github.com/quic-go/quic-go v0.54.0
|
||||||
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
github.com/sagernet/asc-go v0.0.0-20241217030726-d563060fe4e1
|
||||||
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a
|
||||||
github.com/sagernet/cors v1.2.1
|
github.com/sagernet/cors v1.2.1
|
||||||
@@ -41,6 +45,7 @@ require (
|
|||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/stretchr/testify v1.10.0
|
github.com/stretchr/testify v1.10.0
|
||||||
|
github.com/tidwall/gjson v1.18.0
|
||||||
github.com/vishvananda/netns v0.0.5
|
github.com/vishvananda/netns v0.0.5
|
||||||
go.uber.org/zap v1.27.0
|
go.uber.org/zap v1.27.0
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
|
||||||
@@ -49,12 +54,25 @@ require (
|
|||||||
golang.org/x/mod v0.27.0
|
golang.org/x/mod v0.27.0
|
||||||
golang.org/x/net v0.43.0
|
golang.org/x/net v0.43.0
|
||||||
golang.org/x/sys v0.35.0
|
golang.org/x/sys v0.35.0
|
||||||
|
golang.org/x/time v0.11.0
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
google.golang.org/grpc v1.73.0
|
google.golang.org/grpc v1.73.0
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/AdguardTeam/golibs v0.32.7 // indirect
|
||||||
|
github.com/ameshkov/dnscrypt/v2 v2.4.0
|
||||||
|
github.com/ameshkov/dnsstamps v1.0.3 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
|
go.uber.org/mock v0.5.0 // indirect
|
||||||
|
golang.org/x/sync v0.16.0 // indirect
|
||||||
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
//replace github.com/sagernet/sing => ../sing
|
//replace github.com/sagernet/sing => ../sing
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -116,20 +134,23 @@ require (
|
|||||||
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
|
||||||
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||||
|
github.com/tevino/abool/v2 v2.1.0 // indirect
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||||
github.com/x448/float16 v0.8.4 // indirect
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
github.com/zeebo/blake3 v0.2.4 // indirect
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap/exp v0.3.0 // indirect
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
|
||||||
golang.org/x/term v0.34.0 // indirect
|
golang.org/x/term v0.34.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
|
||||||
golang.org/x/time v0.9.0 // indirect
|
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/sagernet/wireguard-go => github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec
|
||||||
|
|
||||||
|
replace github.com/sagernet/sing-dns => github.com/shtorm-7/sing-dns v0.4.6-extended-1.0.0
|
||||||
|
|
||||||
|
replace github.com/ameshkov/dnscrypt/v2 => github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0
|
||||||
|
|||||||
32
go.sum
32
go.sum
@@ -1,11 +1,15 @@
|
|||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
|
github.com/AdguardTeam/golibs v0.32.7 h1:3dmGlAVgmvquCCwHsvEl58KKcRAK3z1UnjMnwSIeDH4=
|
||||||
|
github.com/AdguardTeam/golibs v0.32.7/go.mod h1:bE8KV1zqTzgZjmjFyBJ9f9O5DEKO717r7e57j1HclJA=
|
||||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1OYVo=
|
||||||
|
github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A=
|
||||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
|
github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
|
||||||
@@ -37,12 +41,16 @@ github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbY
|
|||||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
|
||||||
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
|
||||||
|
github.com/enfein/mieru/v3 v3.17.1 h1:pIKbspsKRYNyUrORVI33t1/yz2syaaUkIanskAbGBHY=
|
||||||
|
github.com/enfein/mieru/v3 v3.17.1/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc=
|
||||||
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg=
|
||||||
|
github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec h1:jXekDkSozctYj5JQlV1mCsIW+qHpIkgSFhOIpgRSB1I=
|
||||||
|
github.com/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec/go.mod h1:akc2Wh+rX9bFFNnHJGsQ8VIV3eJI1LXJYgx2Y+8lcW8=
|
||||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||||
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
|
||||||
@@ -145,6 +153,8 @@ github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyf
|
|||||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||||
|
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||||
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||||
@@ -187,10 +197,10 @@ github.com/sagernet/smux v1.5.34-mod.2 h1:gkmBjIjlJ2zQKpLigOkFur5kBKdV6bNRoFu2Wk
|
|||||||
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
|
github.com/sagernet/smux v1.5.34-mod.2/go.mod h1:0KW0+R+ycvA2INW4gbsd7BNyg+HEfLIAxa5N02/28Zc=
|
||||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1 h1:gMC0q+0VvZBotZMZ9G0R8ZMEIT/Q6KnXbw0/OgMjmdk=
|
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1 h1:gMC0q+0VvZBotZMZ9G0R8ZMEIT/Q6KnXbw0/OgMjmdk=
|
||||||
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
github.com/sagernet/tailscale v1.80.3-sing-box-1.12-mod.1/go.mod h1:EBxXsWu4OH2ELbQLq32WoBeIubG8KgDrg4/Oaxjs6lI=
|
||||||
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 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||||
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||||
|
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0 h1:e5s7RKBd2rIPR0StbvZ2vTVtJ5jDTsTk5wtIIapZTRg=
|
||||||
|
github.com/shtorm-7/dnscrypt/v2 v2.4.0-extended-1.0.0/go.mod h1:WpEFV2uhebXb8Jhes/5/fSdpmhGV8TL22RDaeWwV6hI=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
@@ -224,6 +234,14 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
|
|||||||
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||||
|
github.com/tevino/abool/v2 v2.1.0 h1:7w+Vf9f/5gmKT4m4qkayb33/92M+Um45F2BkHOR+L/c=
|
||||||
|
github.com/tevino/abool/v2 v2.1.0/go.mod h1:+Lmlqk6bHDWHqN1cbxqhwEAwMPXgc8I1SDEamtseuXY=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
@@ -251,6 +269,8 @@ go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt
|
|||||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||||
|
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||||
@@ -294,8 +314,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
|||||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||||
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
|
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||||
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
@@ -322,7 +342,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||||
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
|
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||||
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||||
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
protocolDNS "github.com/sagernet/sing-box/protocol/dns"
|
protocolDNS "github.com/sagernet/sing-box/protocol/dns"
|
||||||
"github.com/sagernet/sing-box/protocol/group"
|
"github.com/sagernet/sing-box/protocol/group"
|
||||||
"github.com/sagernet/sing-box/protocol/http"
|
"github.com/sagernet/sing-box/protocol/http"
|
||||||
|
"github.com/sagernet/sing-box/protocol/mieru"
|
||||||
"github.com/sagernet/sing-box/protocol/mixed"
|
"github.com/sagernet/sing-box/protocol/mixed"
|
||||||
"github.com/sagernet/sing-box/protocol/naive"
|
"github.com/sagernet/sing-box/protocol/naive"
|
||||||
"github.com/sagernet/sing-box/protocol/redirect"
|
"github.com/sagernet/sing-box/protocol/redirect"
|
||||||
@@ -33,6 +34,7 @@ import (
|
|||||||
"github.com/sagernet/sing-box/protocol/tor"
|
"github.com/sagernet/sing-box/protocol/tor"
|
||||||
"github.com/sagernet/sing-box/protocol/trojan"
|
"github.com/sagernet/sing-box/protocol/trojan"
|
||||||
"github.com/sagernet/sing-box/protocol/tun"
|
"github.com/sagernet/sing-box/protocol/tun"
|
||||||
|
"github.com/sagernet/sing-box/protocol/tunnel"
|
||||||
"github.com/sagernet/sing-box/protocol/vless"
|
"github.com/sagernet/sing-box/protocol/vless"
|
||||||
"github.com/sagernet/sing-box/protocol/vmess"
|
"github.com/sagernet/sing-box/protocol/vmess"
|
||||||
"github.com/sagernet/sing-box/service/resolved"
|
"github.com/sagernet/sing-box/service/resolved"
|
||||||
@@ -90,6 +92,7 @@ func OutboundRegistry() *outbound.Registry {
|
|||||||
ssh.RegisterOutbound(registry)
|
ssh.RegisterOutbound(registry)
|
||||||
shadowtls.RegisterOutbound(registry)
|
shadowtls.RegisterOutbound(registry)
|
||||||
vless.RegisterOutbound(registry)
|
vless.RegisterOutbound(registry)
|
||||||
|
mieru.RegisterOutbound(registry)
|
||||||
anytls.RegisterOutbound(registry)
|
anytls.RegisterOutbound(registry)
|
||||||
|
|
||||||
registerQUICOutbounds(registry)
|
registerQUICOutbounds(registry)
|
||||||
@@ -102,6 +105,9 @@ func OutboundRegistry() *outbound.Registry {
|
|||||||
func EndpointRegistry() *endpoint.Registry {
|
func EndpointRegistry() *endpoint.Registry {
|
||||||
registry := endpoint.NewRegistry()
|
registry := endpoint.NewRegistry()
|
||||||
|
|
||||||
|
tunnel.RegisterServerEndpoint(registry)
|
||||||
|
tunnel.RegisterClientEndpoint(registry)
|
||||||
|
|
||||||
registerWireGuardEndpoint(registry)
|
registerWireGuardEndpoint(registry)
|
||||||
registerTailscaleEndpoint(registry)
|
registerTailscaleEndpoint(registry)
|
||||||
|
|
||||||
@@ -115,6 +121,7 @@ func DNSTransportRegistry() *dns.TransportRegistry {
|
|||||||
transport.RegisterUDP(registry)
|
transport.RegisterUDP(registry)
|
||||||
transport.RegisterTLS(registry)
|
transport.RegisterTLS(registry)
|
||||||
transport.RegisterHTTPS(registry)
|
transport.RegisterHTTPS(registry)
|
||||||
|
transport.RegisterSDNS(registry)
|
||||||
hosts.RegisterTransport(registry)
|
hosts.RegisterTransport(registry)
|
||||||
local.RegisterTransport(registry)
|
local.RegisterTransport(registry)
|
||||||
fakeip.RegisterTransport(registry)
|
fakeip.RegisterTransport(registry)
|
||||||
|
|||||||
@@ -14,4 +14,5 @@ func registerWireGuardOutbound(registry *outbound.Registry) {
|
|||||||
|
|
||||||
func registerWireGuardEndpoint(registry *endpoint.Registry) {
|
func registerWireGuardEndpoint(registry *endpoint.Registry) {
|
||||||
wireguard.RegisterEndpoint(registry)
|
wireguard.RegisterEndpoint(registry)
|
||||||
|
wireguard.RegisterWARPEndpoint(registry)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -321,6 +321,19 @@ func (o *DNSServerOptions) Upgrade(ctx context.Context) error {
|
|||||||
fakeipOptions.Inet6Range = legacyOptions.Inet6Range
|
fakeipOptions.Inet6Range = legacyOptions.Inet6Range
|
||||||
}
|
}
|
||||||
o.Options = &fakeipOptions
|
o.Options = &fakeipOptions
|
||||||
|
case C.DNSTypeSDNS:
|
||||||
|
o.Type = C.DNSTypeSDNS
|
||||||
|
if serverURL == nil {
|
||||||
|
return E.New("invalid server address")
|
||||||
|
}
|
||||||
|
serverAddr := M.ParseSocksaddr(serverURL.Host)
|
||||||
|
if !serverAddr.IsValid() {
|
||||||
|
return E.New("invalid server address")
|
||||||
|
}
|
||||||
|
o.Options = &SDNSDNSServerOptions{
|
||||||
|
RemoteDNSServerOptions: remoteOptions,
|
||||||
|
Stamp: serverAddr.AddrString(),
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return E.New("unsupported DNS server scheme: ", serverType)
|
return E.New("unsupported DNS server scheme: ", serverType)
|
||||||
}
|
}
|
||||||
@@ -400,3 +413,8 @@ type DHCPDNSServerOptions struct {
|
|||||||
LocalDNSServerOptions
|
LocalDNSServerOptions
|
||||||
Interface string `json:"interface,omitempty"`
|
Interface string `json:"interface,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SDNSDNSServerOptions struct {
|
||||||
|
RemoteDNSServerOptions
|
||||||
|
Stamp string `json:"stamp"`
|
||||||
|
}
|
||||||
@@ -3,19 +3,21 @@ package option
|
|||||||
import "github.com/sagernet/sing/common/json/badoption"
|
import "github.com/sagernet/sing/common/json/badoption"
|
||||||
|
|
||||||
type ExperimentalOptions struct {
|
type ExperimentalOptions struct {
|
||||||
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
|
CacheFile *CacheFileOptions `json:"cache_file,omitempty"`
|
||||||
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
|
ClashAPI *ClashAPIOptions `json:"clash_api,omitempty"`
|
||||||
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
|
V2RayAPI *V2RayAPIOptions `json:"v2ray_api,omitempty"`
|
||||||
Debug *DebugOptions `json:"debug,omitempty"`
|
UnifiedDelay *UnifiedDelayOptions `json:"unified_delay,omitempty"`
|
||||||
|
Debug *DebugOptions `json:"debug,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CacheFileOptions struct {
|
type CacheFileOptions struct {
|
||||||
Enabled bool `json:"enabled,omitempty"`
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
CacheID string `json:"cache_id,omitempty"`
|
CacheID string `json:"cache_id,omitempty"`
|
||||||
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
StoreFakeIP bool `json:"store_fakeip,omitempty"`
|
||||||
StoreRDRC bool `json:"store_rdrc,omitempty"`
|
StoreRDRC bool `json:"store_rdrc,omitempty"`
|
||||||
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
|
StoreWARPConfig bool `json:"store_warp_config,omitempty"`
|
||||||
|
RDRCTimeout badoption.Duration `json:"rdrc_timeout,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClashAPIOptions struct {
|
type ClashAPIOptions struct {
|
||||||
@@ -52,3 +54,7 @@ type V2RayStatsServiceOptions struct {
|
|||||||
Outbounds []string `json:"outbounds,omitempty"`
|
Outbounds []string `json:"outbounds,omitempty"`
|
||||||
Users []string `json:"users,omitempty"`
|
Users []string `json:"users,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UnifiedDelayOptions struct {
|
||||||
|
Enabled bool `json:"enabled,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
13
option/mieru.go
Normal file
13
option/mieru.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing/common/json/badoption"
|
||||||
|
|
||||||
|
type MieruOutboundOptions struct {
|
||||||
|
DialerOptions
|
||||||
|
ServerOptions
|
||||||
|
ServerPortRanges badoption.Listable[string] `json:"server_ports,omitempty"`
|
||||||
|
Transport string `json:"transport,omitempty"`
|
||||||
|
UserName string `json:"username,omitempty"`
|
||||||
|
Password string `json:"password,omitempty"`
|
||||||
|
Multiplexing string `json:"multiplexing,omitempty"`
|
||||||
|
}
|
||||||
@@ -88,6 +88,8 @@ type RawDefaultRule struct {
|
|||||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||||
|
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
|
||||||
|
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
|
||||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||||
|
|||||||
@@ -146,8 +146,9 @@ type RouteActionOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RawRouteOptionsActionOptions struct {
|
type RawRouteOptionsActionOptions struct {
|
||||||
OverrideAddress string `json:"override_address,omitempty"`
|
OverrideAddress string `json:"override_address,omitempty"`
|
||||||
OverridePort uint16 `json:"override_port,omitempty"`
|
OverridePort uint16 `json:"override_port,omitempty"`
|
||||||
|
OverrideTunnelDestination string `json:"override_tunnel_destination,omitempty"`
|
||||||
|
|
||||||
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
NetworkStrategy *NetworkStrategy `json:"network_strategy,omitempty"`
|
||||||
FallbackDelay uint32 `json:"fallback_delay,omitempty"`
|
FallbackDelay uint32 `json:"fallback_delay,omitempty"`
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ type RawDefaultDNSRule struct {
|
|||||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||||
|
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
|
||||||
|
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
|
||||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ type DefaultHeadlessRule struct {
|
|||||||
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
SourcePortRange badoption.Listable[string] `json:"source_port_range,omitempty"`
|
||||||
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
Port badoption.Listable[uint16] `json:"port,omitempty"`
|
||||||
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
PortRange badoption.Listable[string] `json:"port_range,omitempty"`
|
||||||
|
TunnelSource badoption.Listable[string] `json:"tunnel_source,omitempty"`
|
||||||
|
TunnelDestination badoption.Listable[string] `json:"tunnel_destination,omitempty"`
|
||||||
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
ProcessName badoption.Listable[string] `json:"process_name,omitempty"`
|
||||||
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
ProcessPath badoption.Listable[string] `json:"process_path,omitempty"`
|
||||||
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
ProcessPathRegex badoption.Listable[string] `json:"process_path_regex,omitempty"`
|
||||||
|
|||||||
21
option/tunnel.go
Normal file
21
option/tunnel.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
import "github.com/sagernet/sing/common/json/badoption"
|
||||||
|
|
||||||
|
type TunnelClientEndpointOptions struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Outbound Outbound `json:"outbound"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TunnelServerEndpointOptions struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Users []TunnelUser `json:"users"`
|
||||||
|
Inbound Inbound `json:"inbound"`
|
||||||
|
ConnectTimeout badoption.Duration `json:"connect_timeout,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TunnelUser struct {
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
}
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
package option
|
package option
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
Xbadoption "github.com/sagernet/sing-box/common/xray/json/badoption"
|
||||||
C "github.com/sagernet/sing-box/constant"
|
C "github.com/sagernet/sing-box/constant"
|
||||||
E "github.com/sagernet/sing/common/exceptions"
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
"github.com/sagernet/sing/common/json"
|
"github.com/sagernet/sing/common/json"
|
||||||
@@ -15,6 +20,7 @@ type _V2RayTransportOptions struct {
|
|||||||
QUICOptions V2RayQUICOptions `json:"-"`
|
QUICOptions V2RayQUICOptions `json:"-"`
|
||||||
GRPCOptions V2RayGRPCOptions `json:"-"`
|
GRPCOptions V2RayGRPCOptions `json:"-"`
|
||||||
HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
|
HTTPUpgradeOptions V2RayHTTPUpgradeOptions `json:"-"`
|
||||||
|
XHTTPOptions V2RayXHTTPOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type V2RayTransportOptions _V2RayTransportOptions
|
type V2RayTransportOptions _V2RayTransportOptions
|
||||||
@@ -32,6 +38,8 @@ func (o V2RayTransportOptions) MarshalJSON() ([]byte, error) {
|
|||||||
v = o.GRPCOptions
|
v = o.GRPCOptions
|
||||||
case C.V2RayTransportTypeHTTPUpgrade:
|
case C.V2RayTransportTypeHTTPUpgrade:
|
||||||
v = o.HTTPUpgradeOptions
|
v = o.HTTPUpgradeOptions
|
||||||
|
case C.V2RayTransportTypeXHTTP:
|
||||||
|
v = o.XHTTPOptions
|
||||||
case "":
|
case "":
|
||||||
return nil, E.New("missing transport type")
|
return nil, E.New("missing transport type")
|
||||||
default:
|
default:
|
||||||
@@ -57,6 +65,8 @@ func (o *V2RayTransportOptions) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &o.GRPCOptions
|
v = &o.GRPCOptions
|
||||||
case C.V2RayTransportTypeHTTPUpgrade:
|
case C.V2RayTransportTypeHTTPUpgrade:
|
||||||
v = &o.HTTPUpgradeOptions
|
v = &o.HTTPUpgradeOptions
|
||||||
|
case C.V2RayTransportTypeXHTTP:
|
||||||
|
v = &o.XHTTPOptions
|
||||||
default:
|
default:
|
||||||
return E.New("unknown transport type: " + o.Type)
|
return E.New("unknown transport type: " + o.Type)
|
||||||
}
|
}
|
||||||
@@ -98,3 +108,145 @@ type V2RayHTTPUpgradeOptions struct {
|
|||||||
Path string `json:"path,omitempty"`
|
Path string `json:"path,omitempty"`
|
||||||
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
Headers badoption.HTTPHeader `json:"headers,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type V2RayXHTTPBaseOptions struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Host string `json:"host,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Headers map[string]string `json:"headers,omitempty"`
|
||||||
|
DomainStrategy DomainStrategy `json:"domain_strategy,omitempty"`
|
||||||
|
XPaddingBytes Xbadoption.Range `json:"x_padding_bytes"`
|
||||||
|
NoGRPCHeader bool `json:"no_grpc_header,omitempty"`
|
||||||
|
NoSSEHeader bool `json:"no_sse_header,omitempty"`
|
||||||
|
ScMaxEachPostBytes Xbadoption.Range `json:"sc_max_each_post_bytes"`
|
||||||
|
ScMinPostsIntervalMs Xbadoption.Range `json:"sc_min_posts_interval_ms"`
|
||||||
|
ScMaxBufferedPosts int64 `json:"sc_max_buffered_posts,omitempty"`
|
||||||
|
ScStreamUpServerSecs Xbadoption.Range `json:"sc_stream_up_server_secs"`
|
||||||
|
Xmux *V2RayXHTTPXmuxOptions `json:"xmux"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type V2RayXHTTPOptions struct {
|
||||||
|
V2RayXHTTPBaseOptions
|
||||||
|
Download *V2RayXHTTPDownloadOptions `json:"download"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type V2RayXHTTPDownloadOptions struct {
|
||||||
|
V2RayXHTTPBaseOptions
|
||||||
|
ServerOptions
|
||||||
|
OutboundTLSOptionsContainer
|
||||||
|
Detour string `json:"detour,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedPath() string {
|
||||||
|
pathAndQuery := strings.SplitN(c.Path, "?", 2)
|
||||||
|
path := pathAndQuery[0]
|
||||||
|
if path == "" || path[0] != '/' {
|
||||||
|
path = "/" + path
|
||||||
|
}
|
||||||
|
if path[len(path)-1] != '/' {
|
||||||
|
path = path + "/"
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedQuery() string {
|
||||||
|
pathAndQuery := strings.SplitN(c.Path, "?", 2)
|
||||||
|
query := ""
|
||||||
|
if len(pathAndQuery) > 1 {
|
||||||
|
query = pathAndQuery[1]
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetRequestHeader(rawURL string) http.Header {
|
||||||
|
header := http.Header{}
|
||||||
|
for k, v := range c.Headers {
|
||||||
|
header.Add(k, v)
|
||||||
|
}
|
||||||
|
u, _ := url.Parse(rawURL)
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc7541.html#appendix-B
|
||||||
|
// h2's HPACK Header Compression feature employs a huffman encoding using a static table.
|
||||||
|
// 'X' is assigned an 8 bit code, so HPACK compression won't change actual padding length on the wire.
|
||||||
|
// https://www.rfc-editor.org/rfc/rfc9204.html#section-4.1.2-2
|
||||||
|
// h3's similar QPACK feature uses the same huffman table.
|
||||||
|
u.RawQuery = "x_padding=" + strings.Repeat("X", int(c.GetNormalizedXPaddingBytes().Rand()))
|
||||||
|
header.Set("Referer", u.String())
|
||||||
|
return header
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedXPaddingBytes() Xbadoption.Range {
|
||||||
|
if c.XPaddingBytes.To == 0 {
|
||||||
|
return Xbadoption.Range{
|
||||||
|
From: 100,
|
||||||
|
To: 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.XPaddingBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxEachPostBytes() Xbadoption.Range {
|
||||||
|
if c.ScMaxEachPostBytes.To == 0 {
|
||||||
|
return Xbadoption.Range{
|
||||||
|
From: 1000000,
|
||||||
|
To: 1000000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ScMaxEachPostBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMinPostsIntervalMs() Xbadoption.Range {
|
||||||
|
if c.ScMinPostsIntervalMs.To == 0 {
|
||||||
|
return Xbadoption.Range{
|
||||||
|
From: 30,
|
||||||
|
To: 30,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ScMinPostsIntervalMs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedScMaxBufferedPosts() int {
|
||||||
|
if c.ScMaxBufferedPosts == 0 {
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(c.ScMaxBufferedPosts)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *V2RayXHTTPBaseOptions) GetNormalizedScStreamUpServerSecs() Xbadoption.Range {
|
||||||
|
if c.ScStreamUpServerSecs.To == 0 {
|
||||||
|
return Xbadoption.Range{
|
||||||
|
From: 20,
|
||||||
|
To: 80,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return c.ScStreamUpServerSecs
|
||||||
|
}
|
||||||
|
|
||||||
|
type V2RayXHTTPXmuxOptions struct {
|
||||||
|
MaxConcurrency Xbadoption.Range `json:"max_concurrency"`
|
||||||
|
MaxConnections Xbadoption.Range `json:"max_connections"`
|
||||||
|
CMaxReuseTimes Xbadoption.Range `json:"c_max_reuse_times"`
|
||||||
|
HMaxRequestTimes Xbadoption.Range `json:"h_max_request_times"`
|
||||||
|
HMaxReusableSecs Xbadoption.Range `json:"h_max_reusable_secs"`
|
||||||
|
HKeepAlivePeriod int64 `json:"h_keep_alive_period"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *V2RayXHTTPXmuxOptions) GetNormalizedMaxConcurrency() Xbadoption.Range {
|
||||||
|
return m.MaxConcurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *V2RayXHTTPXmuxOptions) GetNormalizedMaxConnections() Xbadoption.Range {
|
||||||
|
return m.MaxConnections
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *V2RayXHTTPXmuxOptions) GetNormalizedCMaxReuseTimes() Xbadoption.Range {
|
||||||
|
return m.CMaxReuseTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxRequestTimes() Xbadoption.Range {
|
||||||
|
return m.HMaxRequestTimes
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *V2RayXHTTPXmuxOptions) GetNormalizedHMaxReusableSecs() Xbadoption.Range {
|
||||||
|
return m.HMaxReusableSecs
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ type WireGuardEndpointOptions struct {
|
|||||||
Peers []WireGuardPeer `json:"peers,omitempty"`
|
Peers []WireGuardPeer `json:"peers,omitempty"`
|
||||||
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||||
Workers int `json:"workers,omitempty"`
|
Workers int `json:"workers,omitempty"`
|
||||||
|
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
||||||
DialerOptions
|
DialerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,25 @@ type WireGuardPeer struct {
|
|||||||
Reserved []uint8 `json:"reserved,omitempty"`
|
Reserved []uint8 `json:"reserved,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WireGuardWARPEndpointOptions struct {
|
||||||
|
System bool `json:"system,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ListenPort uint16 `json:"listen_port,omitempty"`
|
||||||
|
UDPTimeout badoption.Duration `json:"udp_timeout,omitempty"`
|
||||||
|
Workers int `json:"workers,omitempty"`
|
||||||
|
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
||||||
|
Profile *WireGuardCloudflareProfile `json:"profile,omitempty"`
|
||||||
|
DialerOptions
|
||||||
|
}
|
||||||
|
|
||||||
|
type WireGuardCloudflareProfile struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
PrivateKey string `json:"private_key,omitempty"`
|
||||||
|
AuthToken string `json:"auth_token,omitempty"`
|
||||||
|
Recreate bool `json:"recreate,omitempty"`
|
||||||
|
Detour string `json:"detour,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type LegacyWireGuardOutboundOptions struct {
|
type LegacyWireGuardOutboundOptions struct {
|
||||||
DialerOptions
|
DialerOptions
|
||||||
SystemInterface bool `json:"system_interface,omitempty"`
|
SystemInterface bool `json:"system_interface,omitempty"`
|
||||||
@@ -38,12 +58,13 @@ type LegacyWireGuardOutboundOptions struct {
|
|||||||
PrivateKey string `json:"private_key"`
|
PrivateKey string `json:"private_key"`
|
||||||
Peers []LegacyWireGuardPeer `json:"peers,omitempty"`
|
Peers []LegacyWireGuardPeer `json:"peers,omitempty"`
|
||||||
ServerOptions
|
ServerOptions
|
||||||
PeerPublicKey string `json:"peer_public_key"`
|
PeerPublicKey string `json:"peer_public_key"`
|
||||||
PreSharedKey string `json:"pre_shared_key,omitempty"`
|
PreSharedKey string `json:"pre_shared_key,omitempty"`
|
||||||
Reserved []uint8 `json:"reserved,omitempty"`
|
Reserved []uint8 `json:"reserved,omitempty"`
|
||||||
Workers int `json:"workers,omitempty"`
|
Workers int `json:"workers,omitempty"`
|
||||||
MTU uint32 `json:"mtu,omitempty"`
|
MTU uint32 `json:"mtu,omitempty"`
|
||||||
Network NetworkList `json:"network,omitempty"`
|
Network NetworkList `json:"network,omitempty"`
|
||||||
|
Amnezia *WireGuardAmnezia `json:"amnezia,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LegacyWireGuardPeer struct {
|
type LegacyWireGuardPeer struct {
|
||||||
@@ -53,3 +74,15 @@ type LegacyWireGuardPeer struct {
|
|||||||
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
AllowedIPs badoption.Listable[netip.Prefix] `json:"allowed_ips,omitempty"`
|
||||||
Reserved []uint8 `json:"reserved,omitempty"`
|
Reserved []uint8 `json:"reserved,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type WireGuardAmnezia struct {
|
||||||
|
JC int `json:"jc,omitempty"`
|
||||||
|
JMin int `json:"jmin,omitempty"`
|
||||||
|
JMax int `json:"jmax,omitempty"`
|
||||||
|
S1 int `json:"s1,omitempty"`
|
||||||
|
S2 int `json:"s2,omitempty"`
|
||||||
|
H1 uint32 `json:"h1,omitempty"`
|
||||||
|
H2 uint32 `json:"h2,omitempty"`
|
||||||
|
H3 uint32 `json:"h3,omitempty"`
|
||||||
|
H4 uint32 `json:"h4,omitempty"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -157,6 +157,7 @@ func (s *Selector) ListenPacket(ctx context.Context, destination M.Socksaddr) (n
|
|||||||
func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
||||||
selected := s.selected.Load()
|
selected := s.selected.Load()
|
||||||
|
conn = s.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
|
||||||
if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler {
|
if outboundHandler, isHandler := selected.(adapter.ConnectionHandlerEx); isHandler {
|
||||||
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
|
outboundHandler.NewConnectionEx(ctx, conn, metadata, onClose)
|
||||||
} else {
|
} else {
|
||||||
@@ -167,6 +168,7 @@ func (s *Selector) NewConnectionEx(ctx context.Context, conn net.Conn, metadata
|
|||||||
func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (s *Selector) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
||||||
selected := s.selected.Load()
|
selected := s.selected.Load()
|
||||||
|
conn = s.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
|
||||||
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler {
|
if outboundHandler, isHandler := selected.(adapter.PacketConnectionHandlerEx); isHandler {
|
||||||
outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose)
|
outboundHandler.NewPacketConnectionEx(ctx, conn, metadata, onClose)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -162,11 +162,13 @@ func (s *URLTest) ListenPacket(ctx context.Context, destination M.Socksaddr) (ne
|
|||||||
|
|
||||||
func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (s *URLTest) NewConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
||||||
|
conn = s.group.interruptGroup.NewConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
|
||||||
s.connection.NewConnection(ctx, s, conn, metadata, onClose)
|
s.connection.NewConnection(ctx, s, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
func (s *URLTest) NewPacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
ctx = interrupt.ContextWithIsExternalConnection(ctx)
|
||||||
|
conn = s.group.interruptGroup.NewSingPacketConn(conn, interrupt.IsExternalConnectionFromContext(ctx))
|
||||||
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
|
s.connection.NewPacketConnection(ctx, s, conn, metadata, onClose)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
245
protocol/mieru/outbound.go
Normal file
245
protocol/mieru/outbound.go
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
package mieru
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
|
||||||
|
mieruclient "github.com/enfein/mieru/v3/apis/client"
|
||||||
|
mierucommon "github.com/enfein/mieru/v3/apis/common"
|
||||||
|
mierumodel "github.com/enfein/mieru/v3/apis/model"
|
||||||
|
mierupb "github.com/enfein/mieru/v3/pkg/appctl/appctlpb"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Outbound struct {
|
||||||
|
outbound.Adapter
|
||||||
|
dialer N.Dialer
|
||||||
|
logger log.ContextLogger
|
||||||
|
client mieruclient.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterOutbound(registry *outbound.Registry) {
|
||||||
|
outbound.Register(registry, C.TypeMieru, NewOutbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MieruOutboundOptions) (adapter.Outbound, error) {
|
||||||
|
outboundDialer, err := dialer.New(ctx, options.DialerOptions, options.ServerIsDomain())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := buildMieruClientConfig(options, mieruDialer{dialer: outboundDialer})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build mieru client config: %w", err)
|
||||||
|
}
|
||||||
|
c := mieruclient.NewClient()
|
||||||
|
if err := c.Store(config); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to store mieru client config: %w", err)
|
||||||
|
}
|
||||||
|
if err := c.Start(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to start mieru client: %w", err)
|
||||||
|
}
|
||||||
|
logger.InfoContext(ctx, "mieru client is started")
|
||||||
|
|
||||||
|
return &Outbound{
|
||||||
|
Adapter: outbound.NewAdapterWithDialerOptions(C.TypeMieru, tag, []string{N.NetworkTCP, N.NetworkUDP}, options.DialerOptions),
|
||||||
|
dialer: outboundDialer,
|
||||||
|
logger: logger,
|
||||||
|
client: c,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Outbound) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
|
metadata.Outbound = o.Tag()
|
||||||
|
metadata.Destination = destination
|
||||||
|
switch N.NetworkName(network) {
|
||||||
|
case N.NetworkTCP:
|
||||||
|
o.logger.InfoContext(ctx, "outbound connection to ", destination)
|
||||||
|
d, err := socksAddrToNetAddrSpec(destination, "tcp")
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "failed to convert destination address")
|
||||||
|
}
|
||||||
|
return o.client.DialContext(ctx, d)
|
||||||
|
case N.NetworkUDP:
|
||||||
|
o.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||||
|
d, err := socksAddrToNetAddrSpec(destination, "udp")
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "failed to convert destination address")
|
||||||
|
}
|
||||||
|
streamConn, err := o.client.DialContext(ctx, d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &streamer{
|
||||||
|
PacketConn: mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(streamConn)),
|
||||||
|
Remote: destination,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Outbound) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
ctx, metadata := adapter.ExtendContext(ctx)
|
||||||
|
metadata.Outbound = o.Tag()
|
||||||
|
metadata.Destination = destination
|
||||||
|
o.logger.InfoContext(ctx, "outbound UoT packet connection to ", destination)
|
||||||
|
d, err := socksAddrToNetAddrSpec(destination, "udp")
|
||||||
|
if err != nil {
|
||||||
|
return nil, E.Cause(err, "failed to convert destination address")
|
||||||
|
}
|
||||||
|
streamConn, err := o.client.DialContext(ctx, d)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return mierucommon.NewUDPAssociateWrapper(mierucommon.NewPacketOverStreamTunnel(streamConn)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Outbound) Close() error {
|
||||||
|
return common.Close(o.client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// mieruDialer is an adapter to mieru dialer interface.
|
||||||
|
type mieruDialer struct {
|
||||||
|
dialer N.Dialer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (md mieruDialer) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
|
addr := M.ParseSocksaddr(address)
|
||||||
|
return md.dialer.DialContext(ctx, network, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ mierucommon.Dialer = (*mieruDialer)(nil)
|
||||||
|
|
||||||
|
// streamer converts a net.PacketConn to a net.Conn.
|
||||||
|
type streamer struct {
|
||||||
|
net.PacketConn
|
||||||
|
Remote net.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ net.Conn = (*streamer)(nil)
|
||||||
|
|
||||||
|
func (s *streamer) Read(b []byte) (n int, err error) {
|
||||||
|
n, _, err = s.PacketConn.ReadFrom(b)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamer) Write(b []byte) (n int, err error) {
|
||||||
|
return s.WriteTo(b, s.Remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *streamer) RemoteAddr() net.Addr {
|
||||||
|
return s.Remote
|
||||||
|
}
|
||||||
|
|
||||||
|
// socksAddrToNetAddrSpec converts a Socksaddr object to NetAddrSpec, and overrides the network.
|
||||||
|
func socksAddrToNetAddrSpec(sa M.Socksaddr, network string) (mierumodel.NetAddrSpec, error) {
|
||||||
|
var nas mierumodel.NetAddrSpec
|
||||||
|
if err := nas.From(sa); err != nil {
|
||||||
|
return nas, err
|
||||||
|
}
|
||||||
|
nas.Net = network
|
||||||
|
return nas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildMieruClientConfig(options option.MieruOutboundOptions, dialer mieruDialer) (*mieruclient.ClientConfig, error) {
|
||||||
|
if err := validateMieruOptions(options); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to validate mieru options: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
transportProtocol := mierupb.TransportProtocol_TCP.Enum()
|
||||||
|
server := &mierupb.ServerEndpoint{}
|
||||||
|
if options.ServerPort != 0 {
|
||||||
|
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
||||||
|
Port: proto.Int32(int32(options.ServerPort)),
|
||||||
|
Protocol: transportProtocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, pr := range options.ServerPortRanges {
|
||||||
|
server.PortBindings = append(server.PortBindings, &mierupb.PortBinding{
|
||||||
|
PortRange: proto.String(pr),
|
||||||
|
Protocol: transportProtocol,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if M.IsDomainName(options.Server) {
|
||||||
|
server.DomainName = proto.String(options.Server)
|
||||||
|
} else {
|
||||||
|
server.IpAddress = proto.String(options.Server)
|
||||||
|
}
|
||||||
|
config := &mieruclient.ClientConfig{
|
||||||
|
Profile: &mierupb.ClientProfile{
|
||||||
|
ProfileName: proto.String("sing-box"),
|
||||||
|
User: &mierupb.User{
|
||||||
|
Name: proto.String(options.UserName),
|
||||||
|
Password: proto.String(options.Password),
|
||||||
|
},
|
||||||
|
Servers: []*mierupb.ServerEndpoint{server},
|
||||||
|
},
|
||||||
|
Dialer: dialer,
|
||||||
|
}
|
||||||
|
if multiplexing, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; ok {
|
||||||
|
config.Profile.Multiplexing = &mierupb.MultiplexingConfig{
|
||||||
|
Level: mierupb.MultiplexingLevel(multiplexing).Enum(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateMieruOptions(options option.MieruOutboundOptions) error {
|
||||||
|
if options.Server == "" {
|
||||||
|
return fmt.Errorf("server is empty")
|
||||||
|
}
|
||||||
|
if options.ServerPort == 0 && len(options.ServerPortRanges) == 0 {
|
||||||
|
return fmt.Errorf("either server_port or server_ports must be set")
|
||||||
|
}
|
||||||
|
for _, pr := range options.ServerPortRanges {
|
||||||
|
begin, end, err := beginAndEndPortFromPortRange(pr)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("invalid server_ports format")
|
||||||
|
}
|
||||||
|
if begin < 1 || begin > 65535 {
|
||||||
|
return fmt.Errorf("begin port must be between 1 and 65535")
|
||||||
|
}
|
||||||
|
if end < 1 || end > 65535 {
|
||||||
|
return fmt.Errorf("end port must be between 1 and 65535")
|
||||||
|
}
|
||||||
|
if begin > end {
|
||||||
|
return fmt.Errorf("begin port must be less than or equal to end port")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if options.Transport != "TCP" {
|
||||||
|
return fmt.Errorf("transport must be TCP")
|
||||||
|
}
|
||||||
|
if options.UserName == "" {
|
||||||
|
return fmt.Errorf("username is empty")
|
||||||
|
}
|
||||||
|
if options.Password == "" {
|
||||||
|
return fmt.Errorf("password is empty")
|
||||||
|
}
|
||||||
|
if options.Multiplexing != "" {
|
||||||
|
if _, ok := mierupb.MultiplexingLevel_value[options.Multiplexing]; !ok {
|
||||||
|
return fmt.Errorf("invalid multiplexing level: %s", options.Multiplexing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func beginAndEndPortFromPortRange(portRange string) (int, int, error) {
|
||||||
|
var begin, end int
|
||||||
|
_, err := fmt.Sscanf(portRange, "%d-%d", &begin, &end)
|
||||||
|
return begin, end, err
|
||||||
|
}
|
||||||
152
protocol/tunnel/client.go
Normal file
152
protocol/tunnel/client.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"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"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterClientEndpoint(registry *endpoint.Registry) {
|
||||||
|
endpoint.Register[option.TunnelClientEndpointOptions](registry, C.TypeTunnelClient, NewClientEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientEndpoint struct {
|
||||||
|
outbound.Adapter
|
||||||
|
ctx context.Context
|
||||||
|
outbound adapter.Outbound
|
||||||
|
router adapter.ConnectionRouterEx
|
||||||
|
logger logger.ContextLogger
|
||||||
|
uuid uuid.UUID
|
||||||
|
key uuid.UUID
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClientEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelClientEndpointOptions) (adapter.Endpoint, error) {
|
||||||
|
clientUUID, err := uuid.FromString(options.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
clientKey, err := uuid.FromString(options.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client := &ClientEndpoint{
|
||||||
|
Adapter: outbound.NewAdapter(C.TypeTunnelClient, tag, []string{N.NetworkTCP}, []string{}),
|
||||||
|
ctx: ctx,
|
||||||
|
router: router,
|
||||||
|
logger: logger,
|
||||||
|
uuid: clientUUID,
|
||||||
|
key: clientKey,
|
||||||
|
}
|
||||||
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
|
outbound, err := outboundRegistry.CreateOutbound(ctx, router, logger, options.Outbound.Tag, options.Outbound.Type, options.Outbound.Options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
client.outbound = outbound
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientEndpoint) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStatePostStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for range 5 {
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.ctx.Done():
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
err := c.startInboundConn()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.ErrorContext(c.ctx, err)
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if network != N.NetworkTCP {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
var destinationUUID *uuid.UUID
|
||||||
|
if metadata := adapter.ContextFrom(ctx); metadata != nil {
|
||||||
|
if metadata.TunnelDestination != "" {
|
||||||
|
uuid, err := uuid.FromString(metadata.TunnelDestination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
destinationUUID = &uuid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if destinationUUID == nil {
|
||||||
|
return nil, E.New("tunnel destination not set")
|
||||||
|
}
|
||||||
|
if *destinationUUID == c.uuid {
|
||||||
|
return nil, E.New("routing loop")
|
||||||
|
}
|
||||||
|
conn, err := c.outbound.DialContext(ctx, N.NetworkTCP, Destination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = WriteRequest(conn, &Request{UUID: c.key, Command: CommandTCP, DestinationUUID: *destinationUUID, Destination: destination})
|
||||||
|
return conn, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientEndpoint) Close() error {
|
||||||
|
return common.Close(c.outbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientEndpoint) startInboundConn() error {
|
||||||
|
conn, err := c.outbound.DialContext(c.ctx, N.NetworkTCP, Destination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = WriteRequest(conn, &Request{UUID: c.key, Command: CommandInbound, Destination: Destination})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
request, err := ReadRequest(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go c.connHandler(conn, request)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClientEndpoint) connHandler(conn net.Conn, request *Request) {
|
||||||
|
metadata := adapter.InboundContext{
|
||||||
|
Source: M.ParseSocksaddr(conn.RemoteAddr().String()),
|
||||||
|
Destination: request.Destination,
|
||||||
|
}
|
||||||
|
if request.UUID == c.uuid {
|
||||||
|
c.logger.ErrorContext(c.ctx, "routing loop")
|
||||||
|
conn.Close()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
metadata.TunnelSource = request.UUID.String()
|
||||||
|
c.router.RouteConnectionEx(c.ctx, conn, metadata, func(it error) {})
|
||||||
|
}
|
||||||
91
protocol/tunnel/protocol.go
Normal file
91
protocol/tunnel/protocol.go
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Version = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CommandInbound = 1
|
||||||
|
CommandTCP = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var Destination = M.Socksaddr{
|
||||||
|
Fqdn: "sp.tunnel.sing-box.arpa",
|
||||||
|
Port: 444,
|
||||||
|
}
|
||||||
|
|
||||||
|
var AddressSerializer = M.NewSerializer(
|
||||||
|
M.AddressFamilyByte(0x01, M.AddressFamilyIPv4),
|
||||||
|
M.AddressFamilyByte(0x03, M.AddressFamilyIPv6),
|
||||||
|
M.AddressFamilyByte(0x02, M.AddressFamilyFqdn),
|
||||||
|
M.PortThenAddress(),
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
UUID uuid.UUID
|
||||||
|
Command byte
|
||||||
|
DestinationUUID uuid.UUID
|
||||||
|
Destination M.Socksaddr
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadRequest(reader io.Reader) (*Request, error) {
|
||||||
|
var request Request
|
||||||
|
var version uint8
|
||||||
|
err := binary.Read(reader, binary.BigEndian, &version)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if version != Version {
|
||||||
|
return nil, E.New("unknown version: ", version)
|
||||||
|
}
|
||||||
|
_, err = io.ReadFull(reader, request.UUID[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &request.Command)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_, err = io.ReadFull(reader, request.DestinationUUID[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
request.Destination, err = AddressSerializer.ReadAddrPort(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WriteRequest(writer io.Writer, request *Request) error {
|
||||||
|
var requestLen int
|
||||||
|
requestLen += 1 // version
|
||||||
|
requestLen += 16 // UUID
|
||||||
|
requestLen += 16 // destinationUUID
|
||||||
|
requestLen += 1 // command
|
||||||
|
requestLen += AddressSerializer.AddrPortLen(request.Destination)
|
||||||
|
buffer := buf.NewSize(requestLen)
|
||||||
|
defer buffer.Release()
|
||||||
|
common.Must(
|
||||||
|
buffer.WriteByte(Version),
|
||||||
|
common.Error(buffer.Write(request.UUID[:])),
|
||||||
|
buffer.WriteByte(request.Command),
|
||||||
|
common.Error(buffer.Write(request.DestinationUUID[:])),
|
||||||
|
)
|
||||||
|
err := AddressSerializer.WriteAddrPort(buffer, request.Destination)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return common.Error(writer.Write(buffer.Bytes()))
|
||||||
|
}
|
||||||
41
protocol/tunnel/router.go
Normal file
41
protocol/tunnel/router.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing/common/logger"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Router struct {
|
||||||
|
adapter.Router
|
||||||
|
logger logger.ContextLogger
|
||||||
|
handler func(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRouter(router adapter.Router, logger logger.ContextLogger, handler func(context.Context, net.Conn, adapter.InboundContext, N.CloseHandlerFunc) error) *Router {
|
||||||
|
return &Router{Router: router, logger: logger, handler: handler}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) RouteConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error {
|
||||||
|
return r.handler(ctx, conn, metadata, func(error) {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) RoutePacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error {
|
||||||
|
return os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) RouteConnectionEx(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
|
if err := r.handler(ctx, conn, metadata, onClose); err != nil {
|
||||||
|
r.logger.ErrorContext(ctx, err)
|
||||||
|
N.CloseOnHandshakeFailure(conn, onClose, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) RoutePacketConnectionEx(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) {
|
||||||
|
r.logger.ErrorContext(ctx, os.ErrInvalid)
|
||||||
|
N.CloseOnHandshakeFailure(conn, onClose, os.ErrInvalid)
|
||||||
|
}
|
||||||
203
protocol/tunnel/server.go
Normal file
203
protocol/tunnel/server.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
package tunnel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"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"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterServerEndpoint(registry *endpoint.Registry) {
|
||||||
|
endpoint.Register[option.TunnelServerEndpointOptions](registry, C.TypeTunnelServer, NewServerEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ServerEndpoint struct {
|
||||||
|
outbound.Adapter
|
||||||
|
logger logger.ContextLogger
|
||||||
|
inbound adapter.Inbound
|
||||||
|
router adapter.Router
|
||||||
|
uuid uuid.UUID
|
||||||
|
users map[uuid.UUID]uuid.UUID
|
||||||
|
keys map[uuid.UUID]uuid.UUID
|
||||||
|
conns map[uuid.UUID]chan net.Conn
|
||||||
|
timeout time.Duration
|
||||||
|
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelServerEndpointOptions) (adapter.Endpoint, error) {
|
||||||
|
serverUUID, err := uuid.FromString(options.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
server := &ServerEndpoint{
|
||||||
|
Adapter: outbound.NewAdapter(C.TypeTunnelServer, tag, []string{N.NetworkTCP}, []string{}),
|
||||||
|
logger: logger,
|
||||||
|
router: router,
|
||||||
|
uuid: serverUUID,
|
||||||
|
}
|
||||||
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
|
inbound, err := inboundRegistry.Create(ctx, NewRouter(router, logger, server.connHandler), logger, options.Inbound.Tag, options.Inbound.Type, options.Inbound.Options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
server.inbound = inbound
|
||||||
|
server.users = make(map[uuid.UUID]uuid.UUID, len(options.Users))
|
||||||
|
server.keys = make(map[uuid.UUID]uuid.UUID, len(options.Users))
|
||||||
|
server.conns = make(map[uuid.UUID]chan net.Conn)
|
||||||
|
for _, user := range options.Users {
|
||||||
|
key, err := uuid.FromString(user.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uuid, err := uuid.FromString(user.UUID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
server.users[key] = uuid
|
||||||
|
server.keys[uuid] = key
|
||||||
|
server.conns[uuid] = make(chan net.Conn, 10)
|
||||||
|
}
|
||||||
|
if options.ConnectTimeout != 0 {
|
||||||
|
server.timeout = time.Duration(options.ConnectTimeout)
|
||||||
|
} else {
|
||||||
|
server.timeout = C.TCPConnectTimeout
|
||||||
|
}
|
||||||
|
return server, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerEndpoint) Start(stage adapter.StartStage) error {
|
||||||
|
return s.inbound.Start(stage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if network != N.NetworkTCP {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
var sourceUUID *uuid.UUID
|
||||||
|
var ch chan net.Conn
|
||||||
|
if metadata := adapter.ContextFrom(ctx); metadata != nil {
|
||||||
|
if metadata.TunnelDestination != "" {
|
||||||
|
tunnelDestination, err := uuid.FromString(metadata.TunnelDestination)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.mtx.Lock()
|
||||||
|
var ok bool
|
||||||
|
ch, ok = s.conns[tunnelDestination]
|
||||||
|
if !ok {
|
||||||
|
return nil, E.New("user ", metadata.TunnelDestination, " not found")
|
||||||
|
}
|
||||||
|
s.mtx.Unlock()
|
||||||
|
}
|
||||||
|
if metadata.TunnelSource != "" {
|
||||||
|
tunnelSource, err := uuid.FromString(metadata.TunnelSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sourceUUID = &tunnelSource
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ch == nil {
|
||||||
|
return nil, E.New("tunnel destination not set")
|
||||||
|
}
|
||||||
|
if sourceUUID == nil {
|
||||||
|
sourceUUID = &s.uuid
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, s.timeout)
|
||||||
|
defer cancel()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case conn := <-ch:
|
||||||
|
err := WriteRequest(conn, &Request{UUID: *sourceUUID, Command: CommandTCP, Destination: destination})
|
||||||
|
if err != nil {
|
||||||
|
s.logger.ErrorContext(ctx, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return conn, nil
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerEndpoint) Close() error {
|
||||||
|
return common.Close(s.inbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerEndpoint) connHandler(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
|
||||||
|
if metadata.Destination != Destination {
|
||||||
|
s.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
request, err := ReadRequest(conn)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if request.Command == CommandInbound {
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
uuid, ok := s.users[request.UUID]
|
||||||
|
if !ok {
|
||||||
|
return E.New("key ", request.UUID.String(), " not found")
|
||||||
|
}
|
||||||
|
ch := s.conns[uuid]
|
||||||
|
select {
|
||||||
|
case ch <- conn:
|
||||||
|
default:
|
||||||
|
oldConn := <-ch
|
||||||
|
oldConn.Close()
|
||||||
|
ch <- conn
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if request.Command == CommandTCP {
|
||||||
|
sourceUUID, ok := s.users[request.UUID]
|
||||||
|
if !ok {
|
||||||
|
return E.New("key ", request.UUID, " not found")
|
||||||
|
}
|
||||||
|
if sourceUUID == request.DestinationUUID {
|
||||||
|
return E.New("routing loop on ", sourceUUID)
|
||||||
|
}
|
||||||
|
s.mtx.Lock()
|
||||||
|
if request.DestinationUUID != s.uuid {
|
||||||
|
_, ok = s.keys[request.DestinationUUID]
|
||||||
|
if !ok {
|
||||||
|
return E.New("user ", sourceUUID, " not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.mtx.Unlock()
|
||||||
|
metadata.Inbound = s.Tag()
|
||||||
|
metadata.InboundType = C.TypeTunnelServer
|
||||||
|
metadata.Destination = request.Destination
|
||||||
|
metadata.TunnelSource = sourceUUID.String()
|
||||||
|
metadata.TunnelDestination = request.DestinationUUID.String()
|
||||||
|
s.router.RouteConnectionEx(ctx, conn, metadata, onClose)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return E.New("command ", request.Command, " not found")
|
||||||
|
}
|
||||||
@@ -65,6 +65,20 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
} else {
|
} else {
|
||||||
udpTimeout = C.UDPTimeout
|
udpTimeout = C.UDPTimeout
|
||||||
}
|
}
|
||||||
|
var amnezia *wireguard.AmneziaOptions
|
||||||
|
if options.Amnezia != nil {
|
||||||
|
amnezia = &wireguard.AmneziaOptions{
|
||||||
|
JC: options.Amnezia.JC,
|
||||||
|
JMin: options.Amnezia.JMin,
|
||||||
|
JMax: options.Amnezia.JMax,
|
||||||
|
S1: options.Amnezia.S1,
|
||||||
|
S2: options.Amnezia.S2,
|
||||||
|
H1: options.Amnezia.H1,
|
||||||
|
H2: options.Amnezia.H2,
|
||||||
|
H3: options.Amnezia.H3,
|
||||||
|
H4: options.Amnezia.H4,
|
||||||
|
}
|
||||||
|
}
|
||||||
wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{
|
wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
@@ -100,6 +114,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
Workers: options.Workers,
|
Workers: options.Workers,
|
||||||
|
Amnezia: amnezia,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
212
protocol/wireguard/endpoint_warp.go
Normal file
212
protocol/wireguard/endpoint_warp.go
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
package wireguard
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/common/cloudflare"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/json/badoption"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
N "github.com/sagernet/sing/common/network"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RegisterWARPEndpoint(registry *endpoint.Registry) {
|
||||||
|
endpoint.Register[option.WireGuardWARPEndpointOptions](registry, C.TypeWARP, NewWARPEndpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WARPEndpoint struct {
|
||||||
|
endpoint.Adapter
|
||||||
|
endpoint adapter.Endpoint
|
||||||
|
startHandler func()
|
||||||
|
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWARPEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.WireGuardWARPEndpointOptions) (adapter.Endpoint, error) {
|
||||||
|
var dependencies []string
|
||||||
|
if options.Detour != "" {
|
||||||
|
dependencies = append(dependencies, options.Detour)
|
||||||
|
}
|
||||||
|
if options.Profile != nil {
|
||||||
|
if options.Profile.Detour != "" {
|
||||||
|
dependencies = append(dependencies, options.Profile.Detour)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
warpEndpoint := &WARPEndpoint{
|
||||||
|
Adapter: endpoint.NewAdapter(C.TypeWARP, tag, []string{N.NetworkTCP, N.NetworkUDP}, dependencies),
|
||||||
|
}
|
||||||
|
warpEndpoint.mtx.Lock()
|
||||||
|
warpEndpoint.startHandler = func() {
|
||||||
|
defer warpEndpoint.mtx.Unlock()
|
||||||
|
cacheFile := service.FromContext[adapter.CacheFile](ctx)
|
||||||
|
var config *C.WARPConfig
|
||||||
|
var err error
|
||||||
|
if !options.Profile.Recreate && cacheFile != nil && cacheFile.StoreWARPConfig() {
|
||||||
|
savedProfile := cacheFile.LoadWARPConfig(tag)
|
||||||
|
if savedProfile != nil {
|
||||||
|
if err = json.Unmarshal(savedProfile.Content, &config); err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if config == nil {
|
||||||
|
var privateKey wgtypes.Key
|
||||||
|
if options.Profile.PrivateKey != "" {
|
||||||
|
privateKey, err = wgtypes.ParseKey(options.Profile.PrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
privateKey, err = wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opts := make([]cloudflare.CloudflareApiOption, 0, 1)
|
||||||
|
if options.Profile != nil {
|
||||||
|
if options.Profile.Detour != "" {
|
||||||
|
detour, ok := service.FromContext[adapter.OutboundManager](ctx).Outbound(options.Profile.Detour)
|
||||||
|
if !ok {
|
||||||
|
logger.ErrorContext(ctx, E.New("outbound detour not found: ", options.Profile.Detour))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
opts = append(opts, cloudflare.WithDialContext(func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||||
|
return detour.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
api := cloudflare.NewCloudflareApi(opts...)
|
||||||
|
var profile *cloudflare.CloudflareProfile
|
||||||
|
if options.Profile.AuthToken != "" && options.Profile.ID != "" {
|
||||||
|
profile, err = api.GetProfile(ctx, options.Profile.AuthToken, options.Profile.ID)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
profile, err = api.CreateProfile(ctx, privateKey.PublicKey().String())
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config = &C.WARPConfig{
|
||||||
|
PrivateKey: privateKey.String(),
|
||||||
|
Interface: profile.Config.Interface,
|
||||||
|
Peers: profile.Config.Peers,
|
||||||
|
}
|
||||||
|
if cacheFile != nil && cacheFile.StoreWARPConfig() {
|
||||||
|
content, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cacheFile.SaveWARPConfig(tag, &adapter.SavedBinary{
|
||||||
|
LastUpdated: time.Now(),
|
||||||
|
Content: content,
|
||||||
|
LastEtag: "",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
peer := config.Peers[0]
|
||||||
|
hostParts := strings.Split(peer.Endpoint.Host, ":")
|
||||||
|
warpEndpoint.endpoint, err = NewEndpoint(
|
||||||
|
ctx,
|
||||||
|
router,
|
||||||
|
logger,
|
||||||
|
tag,
|
||||||
|
option.WireGuardEndpointOptions{
|
||||||
|
System: options.System,
|
||||||
|
Name: options.Name,
|
||||||
|
ListenPort: options.ListenPort,
|
||||||
|
UDPTimeout: options.UDPTimeout,
|
||||||
|
Workers: options.Workers,
|
||||||
|
Amnezia: options.Amnezia,
|
||||||
|
DialerOptions: options.DialerOptions,
|
||||||
|
|
||||||
|
Address: badoption.Listable[netip.Prefix]{
|
||||||
|
netip.MustParsePrefix(config.Interface.Addresses.V4 + "/32"),
|
||||||
|
netip.MustParsePrefix(config.Interface.Addresses.V6 + "/128"),
|
||||||
|
},
|
||||||
|
PrivateKey: config.PrivateKey,
|
||||||
|
Peers: []option.WireGuardPeer{
|
||||||
|
{
|
||||||
|
Address: hostParts[0],
|
||||||
|
Port: uint16(peer.Endpoint.Ports[rand.Intn(len(peer.Endpoint.Ports))]),
|
||||||
|
PublicKey: peer.PublicKey,
|
||||||
|
AllowedIPs: badoption.Listable[netip.Prefix]{
|
||||||
|
netip.MustParsePrefix("0.0.0.0/0"),
|
||||||
|
netip.MustParsePrefix("::/0"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MTU: 1280,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = warpEndpoint.endpoint.Start(adapter.StartStateStart); err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err = warpEndpoint.endpoint.Start(adapter.StartStatePostStart); err != nil {
|
||||||
|
logger.ErrorContext(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return warpEndpoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WARPEndpoint) Start(stage adapter.StartStage) error {
|
||||||
|
if stage != adapter.StartStatePostStart {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
go w.startHandler()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WARPEndpoint) Close() error {
|
||||||
|
if ok := w.isEndpointInitialized(); !ok {
|
||||||
|
return E.New("endpoint not initialized")
|
||||||
|
}
|
||||||
|
return w.endpoint.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WARPEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
|
||||||
|
if ok := w.isEndpointInitialized(); !ok {
|
||||||
|
return nil, E.New("endpoint not initialized")
|
||||||
|
}
|
||||||
|
return w.endpoint.DialContext(ctx, network, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WARPEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
|
||||||
|
if ok := w.isEndpointInitialized(); !ok {
|
||||||
|
return nil, E.New("endpoint not initialized")
|
||||||
|
}
|
||||||
|
return w.endpoint.ListenPacket(ctx, destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *WARPEndpoint) isEndpointInitialized() bool {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
return w.endpoint != nil
|
||||||
|
}
|
||||||
@@ -79,6 +79,20 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
Reserved: options.Reserved,
|
Reserved: options.Reserved,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
var amnezia *wireguard.AmneziaOptions
|
||||||
|
if options.Amnezia != nil {
|
||||||
|
amnezia = &wireguard.AmneziaOptions{
|
||||||
|
JC: options.Amnezia.JC,
|
||||||
|
JMin: options.Amnezia.JMin,
|
||||||
|
JMax: options.Amnezia.JMax,
|
||||||
|
S1: options.Amnezia.S1,
|
||||||
|
S2: options.Amnezia.S2,
|
||||||
|
H1: options.Amnezia.H1,
|
||||||
|
H2: options.Amnezia.H2,
|
||||||
|
H3: options.Amnezia.H3,
|
||||||
|
H4: options.Amnezia.H4,
|
||||||
|
}
|
||||||
|
}
|
||||||
wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{
|
wgEndpoint, err := wireguard.NewEndpoint(wireguard.EndpointOptions{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
@@ -102,6 +116,7 @@ func NewOutbound(ctx context.Context, router adapter.Router, logger log.ContextL
|
|||||||
},
|
},
|
||||||
Peers: peers,
|
Peers: peers,
|
||||||
Workers: options.Workers,
|
Workers: options.Workers,
|
||||||
|
Amnezia: amnezia,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user