mirror of
https://github.com/MetaCubeX/mihomo.git
synced 2026-02-26 16:57:08 +00:00
feat: add Sudoku protocol inbound & outbound support (#2397)
This commit is contained in:
22
listener/config/sudoku.go
Normal file
22
listener/config/sudoku.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package config
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// SudokuServer describes a Sudoku inbound server configuration.
|
||||
// It is internal to the listener layer and mainly used for logging and wiring.
|
||||
type SudokuServer struct {
|
||||
Enable bool `json:"enable"`
|
||||
Listen string `json:"listen"`
|
||||
Key string `json:"key"`
|
||||
AEADMethod string `json:"aead-method,omitempty"`
|
||||
PaddingMin *int `json:"padding-min,omitempty"`
|
||||
PaddingMax *int `json:"padding-max,omitempty"`
|
||||
Seed string `json:"seed,omitempty"`
|
||||
TableType string `json:"table-type,omitempty"`
|
||||
HandshakeTimeoutSecond *int `json:"handshake-timeout,omitempty"`
|
||||
}
|
||||
|
||||
func (s SudokuServer) String() string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
128
listener/inbound/sudoku.go
Normal file
128
listener/inbound/sudoku.go
Normal file
@@ -0,0 +1,128 @@
|
||||
package inbound
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
sudokuListener "github.com/metacubex/mihomo/listener/sudoku"
|
||||
"github.com/metacubex/mihomo/log"
|
||||
)
|
||||
|
||||
type SudokuOption struct {
|
||||
BaseOption
|
||||
Key string `inbound:"key"`
|
||||
AEADMethod string `inbound:"aead-method,omitempty"`
|
||||
PaddingMin *int `inbound:"padding-min,omitempty"`
|
||||
PaddingMax *int `inbound:"padding-max,omitempty"`
|
||||
Seed string `inbound:"seed,omitempty"`
|
||||
TableType string `inbound:"table-type,omitempty"` // "prefer_ascii" or "prefer_entropy"
|
||||
HandshakeTimeoutSecond *int `inbound:"handshake-timeout,omitempty"`
|
||||
}
|
||||
|
||||
func (o SudokuOption) Equal(config C.InboundConfig) bool {
|
||||
return optionToString(o) == optionToString(config)
|
||||
}
|
||||
|
||||
type Sudoku struct {
|
||||
*Base
|
||||
config *SudokuOption
|
||||
listeners []*sudokuListener.Listener
|
||||
serverConf LC.SudokuServer
|
||||
}
|
||||
|
||||
func NewSudoku(options *SudokuOption) (*Sudoku, error) {
|
||||
if options.Key == "" {
|
||||
return nil, fmt.Errorf("sudoku inbound requires key")
|
||||
}
|
||||
base, err := NewBase(&options.BaseOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultConf := apis.DefaultConfig()
|
||||
|
||||
serverConf := LC.SudokuServer{
|
||||
Enable: true,
|
||||
Listen: base.RawAddress(),
|
||||
Key: options.Key,
|
||||
AEADMethod: options.AEADMethod,
|
||||
PaddingMin: options.PaddingMin,
|
||||
PaddingMax: options.PaddingMax,
|
||||
Seed: options.Seed,
|
||||
TableType: options.TableType,
|
||||
}
|
||||
if options.HandshakeTimeoutSecond != nil {
|
||||
serverConf.HandshakeTimeoutSecond = options.HandshakeTimeoutSecond
|
||||
} else {
|
||||
// Use Sudoku default if not specified.
|
||||
v := defaultConf.HandshakeTimeoutSeconds
|
||||
serverConf.HandshakeTimeoutSecond = &v
|
||||
}
|
||||
|
||||
return &Sudoku{
|
||||
Base: base,
|
||||
config: options,
|
||||
serverConf: serverConf,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Config implements constant.InboundListener
|
||||
func (s *Sudoku) Config() C.InboundConfig {
|
||||
return s.config
|
||||
}
|
||||
|
||||
// Address implements constant.InboundListener
|
||||
func (s *Sudoku) Address() string {
|
||||
var addrList []string
|
||||
for _, l := range s.listeners {
|
||||
addrList = append(addrList, l.Address())
|
||||
}
|
||||
return strings.Join(addrList, ",")
|
||||
}
|
||||
|
||||
// Listen implements constant.InboundListener
|
||||
func (s *Sudoku) Listen(tunnel C.Tunnel) error {
|
||||
if s.serverConf.Key == "" {
|
||||
return fmt.Errorf("sudoku inbound requires key")
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, addr := range strings.Split(s.RawAddress(), ",") {
|
||||
conf := s.serverConf
|
||||
conf.Listen = addr
|
||||
|
||||
l, err := sudokuListener.New(conf, tunnel, s.Additions()...)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
s.listeners = append(s.listeners, l)
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
|
||||
log.Infoln("Sudoku[%s] inbound listening at: %s", s.Name(), s.Address())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements constant.InboundListener
|
||||
func (s *Sudoku) Close() error {
|
||||
var errs []error
|
||||
for _, l := range s.listeners {
|
||||
if err := l.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var _ C.InboundListener = (*Sudoku)(nil)
|
||||
91
listener/inbound/sudoku_test.go
Normal file
91
listener/inbound/sudoku_test.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package inbound_test
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"testing"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/outbound"
|
||||
"github.com/metacubex/mihomo/listener/inbound"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func testInboundSudoku(t *testing.T, inboundOptions inbound.SudokuOption, outboundOptions outbound.SudokuOption) {
|
||||
t.Parallel()
|
||||
|
||||
inboundOptions.BaseOption = inbound.BaseOption{
|
||||
NameStr: "sudoku_inbound",
|
||||
Listen: "127.0.0.1",
|
||||
Port: "0",
|
||||
}
|
||||
in, err := inbound.NewSudoku(&inboundOptions)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
tunnel := NewHttpTestTunnel()
|
||||
defer tunnel.Close()
|
||||
|
||||
err = in.Listen(tunnel)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
addrPort, err := netip.ParseAddrPort(in.Address())
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
outboundOptions.Name = "sudoku_outbound"
|
||||
outboundOptions.Server = addrPort.Addr().String()
|
||||
outboundOptions.Port = int(addrPort.Port())
|
||||
|
||||
out, err := outbound.NewSudoku(outboundOptions)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
tunnel.DoTest(t, out)
|
||||
}
|
||||
|
||||
func TestInboundSudoku_Basic(t *testing.T) {
|
||||
key := "test_key"
|
||||
inboundOptions := inbound.SudokuOption{
|
||||
Key: key,
|
||||
}
|
||||
outboundOptions := outbound.SudokuOption{
|
||||
Key: key,
|
||||
}
|
||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||
}
|
||||
|
||||
func TestInboundSudoku_Entropy(t *testing.T) {
|
||||
key := "test_key_entropy"
|
||||
inboundOptions := inbound.SudokuOption{
|
||||
Key: key,
|
||||
TableType: "prefer_entropy",
|
||||
}
|
||||
outboundOptions := outbound.SudokuOption{
|
||||
Key: key,
|
||||
TableType: "prefer_entropy",
|
||||
}
|
||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||
}
|
||||
|
||||
func TestInboundSudoku_Padding(t *testing.T) {
|
||||
key := "test_key_padding"
|
||||
min := 10
|
||||
max := 100
|
||||
inboundOptions := inbound.SudokuOption{
|
||||
Key: key,
|
||||
PaddingMin: &min,
|
||||
PaddingMax: &max,
|
||||
}
|
||||
outboundOptions := outbound.SudokuOption{
|
||||
Key: key,
|
||||
PaddingMin: &min,
|
||||
PaddingMax: &max,
|
||||
}
|
||||
testInboundSudoku(t, inboundOptions, outboundOptions)
|
||||
}
|
||||
@@ -134,6 +134,13 @@ func ParseListener(mapping map[string]any) (C.InboundListener, error) {
|
||||
return nil, err
|
||||
}
|
||||
listener, err = IN.NewMieru(mieruOption)
|
||||
case "sudoku":
|
||||
sudokuOption := &IN.SudokuOption{}
|
||||
err = decoder.Decode(mapping, sudokuOption)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
listener, err = IN.NewSudoku(sudokuOption)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupport proxy type: %s", proxyType)
|
||||
}
|
||||
|
||||
139
listener/sudoku/server.go
Normal file
139
listener/sudoku/server.go
Normal file
@@ -0,0 +1,139 @@
|
||||
package sudoku
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/saba-futai/sudoku/apis"
|
||||
sudokuobfs "github.com/saba-futai/sudoku/pkg/obfs/sudoku"
|
||||
|
||||
"github.com/metacubex/mihomo/adapter/inbound"
|
||||
C "github.com/metacubex/mihomo/constant"
|
||||
LC "github.com/metacubex/mihomo/listener/config"
|
||||
"github.com/metacubex/mihomo/transport/socks5"
|
||||
)
|
||||
|
||||
type Listener struct {
|
||||
listener net.Listener
|
||||
addr string
|
||||
closed bool
|
||||
protoConf apis.ProtocolConfig
|
||||
}
|
||||
|
||||
// RawAddress implements C.Listener
|
||||
func (l *Listener) RawAddress() string {
|
||||
return l.addr
|
||||
}
|
||||
|
||||
// Address implements C.Listener
|
||||
func (l *Listener) Address() string {
|
||||
if l.listener == nil {
|
||||
return ""
|
||||
}
|
||||
return l.listener.Addr().String()
|
||||
}
|
||||
|
||||
// Close implements C.Listener
|
||||
func (l *Listener) Close() error {
|
||||
l.closed = true
|
||||
if l.listener != nil {
|
||||
return l.listener.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Listener) handleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
|
||||
tunnelConn, target, err := apis.ServerHandshake(conn, &l.protoConf)
|
||||
if err != nil {
|
||||
_ = conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
targetAddr := socks5.ParseAddr(target)
|
||||
if targetAddr == nil {
|
||||
_ = tunnelConn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
tunnel.HandleTCPConn(inbound.NewSocket(targetAddr, tunnelConn, C.SUDOKU, additions...))
|
||||
}
|
||||
|
||||
func New(config LC.SudokuServer, tunnel C.Tunnel, additions ...inbound.Addition) (*Listener, error) {
|
||||
if len(additions) == 0 {
|
||||
additions = []inbound.Addition{
|
||||
inbound.WithInName("DEFAULT-SUDOKU"),
|
||||
inbound.WithSpecialRules(""),
|
||||
}
|
||||
}
|
||||
|
||||
l, err := inbound.Listen("tcp", config.Listen)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
seed := config.Seed
|
||||
if seed == "" {
|
||||
seed = config.Key
|
||||
}
|
||||
|
||||
tableType := strings.ToLower(config.TableType)
|
||||
if tableType == "" {
|
||||
tableType = "prefer_ascii"
|
||||
}
|
||||
|
||||
table := sudokuobfs.NewTable(seed, tableType)
|
||||
|
||||
defaultConf := apis.DefaultConfig()
|
||||
paddingMin := defaultConf.PaddingMin
|
||||
paddingMax := defaultConf.PaddingMax
|
||||
if config.PaddingMin != nil {
|
||||
paddingMin = *config.PaddingMin
|
||||
}
|
||||
if config.PaddingMax != nil {
|
||||
paddingMax = *config.PaddingMax
|
||||
}
|
||||
if config.PaddingMin == nil && config.PaddingMax != nil && paddingMax < paddingMin {
|
||||
paddingMin = paddingMax
|
||||
}
|
||||
if config.PaddingMax == nil && config.PaddingMin != nil && paddingMax < paddingMin {
|
||||
paddingMax = paddingMin
|
||||
}
|
||||
|
||||
handshakeTimeout := defaultConf.HandshakeTimeoutSeconds
|
||||
if config.HandshakeTimeoutSecond != nil {
|
||||
handshakeTimeout = *config.HandshakeTimeoutSecond
|
||||
}
|
||||
|
||||
protoConf := apis.ProtocolConfig{
|
||||
Key: config.Key,
|
||||
AEADMethod: defaultConf.AEADMethod,
|
||||
Table: table,
|
||||
PaddingMin: paddingMin,
|
||||
PaddingMax: paddingMax,
|
||||
HandshakeTimeoutSeconds: handshakeTimeout,
|
||||
}
|
||||
if config.AEADMethod != "" {
|
||||
protoConf.AEADMethod = config.AEADMethod
|
||||
}
|
||||
|
||||
sl := &Listener{
|
||||
listener: l,
|
||||
addr: config.Listen,
|
||||
protoConf: protoConf,
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if sl.closed {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
go sl.handleConn(c, tunnel, additions...)
|
||||
}
|
||||
}()
|
||||
|
||||
return sl, nil
|
||||
}
|
||||
Reference in New Issue
Block a user