Skip to content

Commit 8b8eae5

Browse files
committed
feat: support simple-obfs for shadowsocks listener
1 parent cc376ac commit 8b8eae5

9 files changed

Lines changed: 372 additions & 25 deletions

File tree

docs/config.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,9 @@ listeners:
15591559
# proxy: proxy # 如果不为空则直接将该入站流量交由指定 proxy 处理 (当 proxy 不为空时,这里的 proxy 名称必须合法,否则会出错)
15601560
password: vlmpIPSyHH6f4S8WVPdRIHIlzmB+GIRfoH3aNJ/t9Gg=
15611561
cipher: 2022-blake3-aes-256-gcm
1562+
# simple-obfs:
1563+
# enable: false # 设置为true时开启
1564+
# mode: http # Available: http, tls
15621565
# shadow-tls:
15631566
# enable: false # 设置为true时开启
15641567
# version: 3 # 支持v1/v2/v3

listener/config/shadowsocks.go

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ import (
77
)
88

99
type ShadowsocksServer struct {
10-
Enable bool
11-
Listen string
12-
Password string
13-
Cipher string
14-
Udp bool
15-
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
16-
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
17-
KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"`
10+
Enable bool
11+
Listen string
12+
Password string
13+
Cipher string
14+
Udp bool
15+
MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"`
16+
ShadowTLS ShadowTLS `yaml:"shadow-tls" json:"shadow-tls,omitempty"`
17+
KcpTun KcpTun `yaml:"kcp-tun" json:"kcp-tun,omitempty"`
18+
SimpleObfs SimpleObfs `yaml:"simple-obfs" json:"simple-obfs,omitempty"`
19+
}
20+
21+
type SimpleObfs struct {
22+
Enable bool
23+
Mode string
1824
}
1925

2026
func (t ShadowsocksServer) String() string {

listener/inbound/shadowsocks.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,25 @@ import (
1111

1212
type ShadowSocksOption struct {
1313
BaseOption
14-
Password string `inbound:"password"`
15-
Cipher string `inbound:"cipher"`
16-
UDP bool `inbound:"udp,omitempty"`
17-
MuxOption MuxOption `inbound:"mux-option,omitempty"`
18-
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
19-
KcpTun KcpTun `inbound:"kcp-tun,omitempty"`
14+
Password string `inbound:"password"`
15+
Cipher string `inbound:"cipher"`
16+
UDP bool `inbound:"udp,omitempty"`
17+
MuxOption MuxOption `inbound:"mux-option,omitempty"`
18+
ShadowTLS ShadowTLS `inbound:"shadow-tls,omitempty"`
19+
KcpTun KcpTun `inbound:"kcp-tun,omitempty"`
20+
SimpleObfs SimpleObfs `inbound:"simple-obfs,omitempty"`
21+
}
22+
23+
type SimpleObfs struct {
24+
Enable bool `inbound:"enable,omitempty"`
25+
Mode string `inbound:"mode,omitempty"`
26+
}
27+
28+
func (o SimpleObfs) Build() LC.SimpleObfs {
29+
return LC.SimpleObfs{
30+
Enable: o.Enable,
31+
Mode: o.Mode,
32+
}
2033
}
2134

2235
func (o ShadowSocksOption) Equal(config C.InboundConfig) bool {
@@ -39,14 +52,15 @@ func NewShadowSocks(options *ShadowSocksOption) (*ShadowSocks, error) {
3952
Base: base,
4053
config: options,
4154
ss: LC.ShadowsocksServer{
42-
Enable: true,
43-
Listen: base.RawAddress(),
44-
Password: options.Password,
45-
Cipher: options.Cipher,
46-
Udp: options.UDP,
47-
MuxOption: options.MuxOption.Build(),
48-
ShadowTLS: options.ShadowTLS.Build(),
49-
KcpTun: options.KcpTun.Build(),
55+
Enable: true,
56+
Listen: base.RawAddress(),
57+
Password: options.Password,
58+
Cipher: options.Cipher,
59+
Udp: options.UDP,
60+
MuxOption: options.MuxOption.Build(),
61+
ShadowTLS: options.ShadowTLS.Build(),
62+
KcpTun: options.KcpTun.Build(),
63+
SimpleObfs: options.SimpleObfs.Build(),
5064
},
5165
}, nil
5266
}

listener/inbound/shadowsocks_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,34 @@ func TestInboundShadowSocks_ShadowTlsv3(t *testing.T) {
167167
testInboundShadowSocksShadowTls(t, inboundOptions, outboundOptions)
168168
}
169169

170+
func TestInboundShadowSocks_SimpleObfs_Http(t *testing.T) {
171+
inboundOptions := inbound.ShadowSocksOption{
172+
SimpleObfs: inbound.SimpleObfs{
173+
Enable: true,
174+
Mode: "http",
175+
},
176+
}
177+
outboundOptions := outbound.ShadowSocksOption{
178+
Plugin: "obfs",
179+
PluginOpts: map[string]any{"mode": "http", "host": realityDest},
180+
}
181+
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
182+
}
183+
184+
func TestInboundShadowSocks_SimpleObfs_Tls(t *testing.T) {
185+
inboundOptions := inbound.ShadowSocksOption{
186+
SimpleObfs: inbound.SimpleObfs{
187+
Enable: true,
188+
Mode: "tls",
189+
},
190+
}
191+
outboundOptions := outbound.ShadowSocksOption{
192+
Plugin: "obfs",
193+
PluginOpts: map[string]any{"mode": "tls", "host": realityDest},
194+
}
195+
testInboundShadowSocks(t, inboundOptions, outboundOptions, shadowsocksCipherShortLists, false)
196+
}
197+
170198
func TestInboundShadowSocks_KcpTun(t *testing.T) {
171199
if runtime.GOOS == "windows" && strings.HasPrefix(runtime.Version(), "go1.20") {
172200
t.Skip("skip kcptun test on windows go1.20")

listener/shadowsocks/tcp.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package shadowsocks
22

33
import (
4+
"fmt"
45
"net"
56
"strings"
67

@@ -10,6 +11,7 @@ import (
1011
LC "github.com/metacubex/mihomo/listener/config"
1112
"github.com/metacubex/mihomo/listener/sing"
1213
"github.com/metacubex/mihomo/transport/shadowsocks/core"
14+
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
1315
"github.com/metacubex/mihomo/transport/socks5"
1416
)
1517

@@ -20,6 +22,7 @@ type Listener struct {
2022
udpListeners []*UDPListener
2123
pickCipher core.Cipher
2224
handler *sing.ListenerHandler
25+
simpleObfs func(net.Conn) net.Conn
2326
}
2427

2528
var _listener *Listener
@@ -40,9 +43,20 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
4043
return nil, err
4144
}
4245

43-
sl := &Listener{false, config, nil, nil, pickCipher, h}
46+
sl := &Listener{config: config, pickCipher: pickCipher, handler: h}
4447
_listener = sl
4548

49+
if config.SimpleObfs.Enable {
50+
switch config.SimpleObfs.Mode {
51+
case "http":
52+
sl.simpleObfs = obfs.NewHTTPObfsServer
53+
case "tls":
54+
sl.simpleObfs = obfs.NewTLSObfsServer
55+
default:
56+
return nil, fmt.Errorf("unsupported simple obfs mode: %s", config.SimpleObfs.Mode)
57+
}
58+
}
59+
4660
for _, addr := range strings.Split(config.Listen, ",") {
4761
addr := addr
4862

@@ -111,6 +125,9 @@ func (l *Listener) AddrList() (addrList []net.Addr) {
111125
}
112126

113127
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
128+
if l.simpleObfs != nil {
129+
conn = l.simpleObfs(conn)
130+
}
114131
conn = l.pickCipher.StreamConn(conn)
115132
conn = N.NewDeadlineConn(conn) // embed ss can't handle readDeadline correctly
116133

listener/sing_shadowsocks/server.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/metacubex/mihomo/log"
1616
"github.com/metacubex/mihomo/ntp"
1717
"github.com/metacubex/mihomo/transport/kcptun"
18+
obfs "github.com/metacubex/mihomo/transport/simple-obfs"
1819

1920
shadowsocks "github.com/metacubex/sing-shadowsocks"
2021
"github.com/metacubex/sing-shadowsocks/shadowaead"
@@ -34,6 +35,7 @@ type Listener struct {
3435
udpListeners []net.PacketConn
3536
service shadowsocks.Service
3637
shadowTLS *shadowtls.Service
38+
simpleObfs func(net.Conn) net.Conn
3739
}
3840

3941
var _listener *Listener
@@ -139,6 +141,17 @@ func New(config LC.ShadowsocksServer, tunnel C.Tunnel, additions ...inbound.Addi
139141
}
140142
}
141143

144+
if config.SimpleObfs.Enable {
145+
switch config.SimpleObfs.Mode {
146+
case "http":
147+
sl.simpleObfs = obfs.NewHTTPObfsServer
148+
case "tls":
149+
sl.simpleObfs = obfs.NewTLSObfsServer
150+
default:
151+
return nil, fmt.Errorf("unsupported simple obfs mode: %s", config.SimpleObfs.Mode)
152+
}
153+
}
154+
142155
var kcptunServer *kcptun.Server
143156
if config.KcpTun.Enable {
144157
kcptunServer = kcptun.NewServer(config.KcpTun.Config)
@@ -268,6 +281,9 @@ func (l *Listener) AddrList() (addrList []net.Addr) {
268281

269282
func (l *Listener) HandleConn(conn net.Conn, tunnel C.Tunnel, additions ...inbound.Addition) {
270283
ctx := sing.WithAdditions(context.TODO(), additions...)
284+
if l.simpleObfs != nil {
285+
conn = l.simpleObfs(conn)
286+
}
271287
err := l.service.NewConnection(ctx, conn, M.Metadata{
272288
Protocol: "shadowsocks",
273289
Source: M.SocksaddrFromNet(conn.RemoteAddr()),

transport/simple-obfs/http.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (ho *HTTPObfs) Write(b []byte) (int, error) {
7070
if err != nil {
7171
return 0, err
7272
}
73-
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", randv2.Int()%54, randv2.Int()%2))
73+
req.Header.Set("User-Agent", fmt.Sprintf("curl/7.%d.%d", randv2.IntN(54), randv2.IntN(2)))
7474
req.Header.Set("Upgrade", "websocket")
7575
req.Header.Set("Connection", "Upgrade")
7676
req.Host = ho.host
@@ -83,7 +83,6 @@ func (ho *HTTPObfs) Write(b []byte) (int, error) {
8383
ho.firstRequest = false
8484
return len(b), err
8585
}
86-
8786
return ho.Conn.Write(b)
8887
}
8988

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package obfs
2+
3+
import (
4+
"bufio"
5+
"crypto/rand"
6+
"encoding/base64"
7+
"fmt"
8+
"io"
9+
"net"
10+
"time"
11+
12+
"github.com/metacubex/http"
13+
"github.com/metacubex/randv2"
14+
)
15+
16+
type HTTPObfsServer struct {
17+
net.Conn
18+
buf []byte
19+
bio *bufio.Reader
20+
offset int
21+
firstRequest bool
22+
firstResponse bool
23+
}
24+
25+
func (hos *HTTPObfsServer) Read(b []byte) (int, error) {
26+
if hos.buf != nil {
27+
n := copy(b, hos.buf[hos.offset:])
28+
hos.offset += n
29+
if hos.offset == len(hos.buf) {
30+
hos.offset = 0
31+
hos.buf = nil
32+
}
33+
return n, nil
34+
}
35+
36+
if hos.firstRequest {
37+
bio := bufio.NewReader(hos.Conn)
38+
req, err := http.ReadRequest(bio)
39+
if err != nil {
40+
return 0, err
41+
}
42+
if req.Method != "GET" || req.Header.Get("Connection") != "Upgrade" {
43+
return 0, io.EOF
44+
}
45+
46+
buf, err := io.ReadAll(req.Body)
47+
if err != nil {
48+
return 0, err
49+
}
50+
n := copy(b, buf)
51+
if n < len(buf) {
52+
hos.buf = buf
53+
hos.offset = n
54+
}
55+
req.Body.Close()
56+
hos.bio = bio
57+
hos.firstRequest = false
58+
return n, nil
59+
}
60+
61+
return hos.bio.Read(b)
62+
}
63+
64+
const httpResponseTemplate = "HTTP/1.1 101 Switching Protocols\r\n" +
65+
"Server: nginx/1.%d.%d\r\n" +
66+
"Date: %s\r\n" +
67+
"Upgrade: websocket\r\n" +
68+
"Connection: Upgrade\r\n" +
69+
"Sec-WebSocket-Accept: %s\r\n" +
70+
"\r\n"
71+
72+
var vMajor = randv2.IntN(11)
73+
var vMinor = randv2.IntN(12)
74+
75+
func (hos *HTTPObfsServer) Write(b []byte) (int, error) {
76+
if hos.firstResponse {
77+
randBytes := make([]byte, 16)
78+
rand.Read(randBytes)
79+
date := time.Now().Format(time.RFC1123)
80+
resp := fmt.Sprintf(httpResponseTemplate, vMajor, vMinor, date, base64.URLEncoding.EncodeToString(randBytes))
81+
_, err := hos.Conn.Write([]byte(resp))
82+
if err != nil {
83+
return 0, err
84+
}
85+
hos.firstResponse = false
86+
}
87+
return hos.Conn.Write(b)
88+
}
89+
90+
func NewHTTPObfsServer(conn net.Conn) net.Conn {
91+
return &HTTPObfsServer{
92+
Conn: conn,
93+
buf: nil,
94+
bio: nil,
95+
offset: 0,
96+
firstRequest: true,
97+
firstResponse: true,
98+
}
99+
}

0 commit comments

Comments
 (0)