mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-05-14 00:51:12 +03:00
Add mieru protocol
This commit is contained in:
@@ -20,6 +20,7 @@ const (
|
||||
TypeTor = "tor"
|
||||
TypeSSH = "ssh"
|
||||
TypeShadowTLS = "shadowtls"
|
||||
TypeMieru = "mieru"
|
||||
TypeShadowsocksR = "shadowsocksr"
|
||||
TypeVLESS = "vless"
|
||||
TypeTUIC = "tuic"
|
||||
@@ -79,6 +80,8 @@ func ProxyDisplayName(proxyType string) string {
|
||||
return "TUIC"
|
||||
case TypeHysteria2:
|
||||
return "Hysteria2"
|
||||
case TypeMieru:
|
||||
return "Mieru"
|
||||
case TypeSelector:
|
||||
return "Selector"
|
||||
case TypeURLTest:
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
| `shadowtls` | [ShadowTLS](./shadowtls/) |
|
||||
| `tuic` | [TUIC](./tuic/) |
|
||||
| `hysteria2` | [Hysteria2](./hysteria2/) |
|
||||
| `mieru` | [Mieru](./mieru/) |
|
||||
| `tor` | [Tor](./tor/) |
|
||||
| `ssh` | [SSH](./ssh/) |
|
||||
| `dns` | [DNS](./dns/) |
|
||||
|
||||
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/)。
|
||||
1
go.mod
1
go.mod
@@ -8,6 +8,7 @@ require (
|
||||
github.com/caddyserver/certmagic v0.20.0
|
||||
github.com/cloudflare/circl v1.6.1
|
||||
github.com/cretz/bine v0.2.0
|
||||
github.com/enfein/mieru/v3 v3.13.0
|
||||
github.com/go-chi/chi/v5 v5.2.1
|
||||
github.com/go-chi/render v1.0.3
|
||||
github.com/gofrs/uuid/v5 v5.3.2
|
||||
|
||||
2
go.sum
2
go.sum
@@ -16,6 +16,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1 h1:CaO/zOnF8VvUfEbhRatPcwKVWamvbYd8tQGRWacE9kU=
|
||||
github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=
|
||||
github.com/enfein/mieru/v3 v3.13.0 h1:eGyxLGkb+lut9ebmx+BGwLJ5UMbEc/wGIYO0AXEKy98=
|
||||
github.com/enfein/mieru/v3 v3.13.0/go.mod h1:zJBUCsi5rxyvHM8fjFf+GLaEl4OEjjBXr1s5F6Qd3hM=
|
||||
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/getlantern/wireguard-go v0.0.1-beta.5.0.20250303165430-793006c422ec h1:jXekDkSozctYj5JQlV1mCsIW+qHpIkgSFhOIpgRSB1I=
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/sagernet/sing-box/protocol/dns"
|
||||
"github.com/sagernet/sing-box/protocol/group"
|
||||
"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/naive"
|
||||
"github.com/sagernet/sing-box/protocol/redirect"
|
||||
@@ -75,6 +76,7 @@ func OutboundRegistry() *outbound.Registry {
|
||||
ssh.RegisterOutbound(registry)
|
||||
shadowtls.RegisterOutbound(registry)
|
||||
vless.RegisterOutbound(registry)
|
||||
mieru.RegisterOutbound(registry)
|
||||
|
||||
registerQUICOutbounds(registry)
|
||||
registerWireGuardOutbound(registry)
|
||||
|
||||
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"`
|
||||
}
|
||||
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)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user