diff --git a/docs/config.yaml b/docs/config.yaml index 8d886055..52ccecf4 100644 --- a/docs/config.yaml +++ b/docs/config.yaml @@ -1161,6 +1161,7 @@ listeners: uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 alterId: 1 # ws-path: "/" # 如果不为空则开启 websocket 传输层 + # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key @@ -1213,6 +1214,7 @@ listeners: uuid: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 flow: xtls-rprx-vision # ws-path: "/" # 如果不为空则开启 websocket 传输层 + # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) # certificate: ./server.crt # private-key: ./server.key @@ -1248,6 +1250,7 @@ listeners: - username: 1 password: 9d0cb9d0-964f-4ef6-897d-6c6b3ccf9e68 # ws-path: "/" # 如果不为空则开启 websocket 传输层 + # grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层 # 下面两项如果填写则开启 tls(需要同时填写) certificate: ./server.crt private-key: ./server.key diff --git a/listener/config/trojan.go b/listener/config/trojan.go index 637266c5..28b6fe7c 100644 --- a/listener/config/trojan.go +++ b/listener/config/trojan.go @@ -13,15 +13,16 @@ type TrojanUser struct { } type TrojanServer struct { - Enable bool - Listen string - Users []TrojanUser - WsPath string - Certificate string - PrivateKey string - RealityConfig reality.Config - MuxOption sing.MuxOption - TrojanSSOption TrojanSSOption + Enable bool + Listen string + Users []TrojanUser + WsPath string + GrpcServiceName string + Certificate string + PrivateKey string + RealityConfig reality.Config + MuxOption sing.MuxOption + TrojanSSOption TrojanSSOption } // TrojanSSOption from https://github.com/p4gefau1t/trojan-go/blob/v0.10.6/tunnel/shadowsocks/config.go#L5 diff --git a/listener/config/vless.go b/listener/config/vless.go index 2f88c1c4..d656db9f 100644 --- a/listener/config/vless.go +++ b/listener/config/vless.go @@ -14,14 +14,15 @@ type VlessUser struct { } type VlessServer struct { - Enable bool - Listen string - Users []VlessUser - WsPath string - Certificate string - PrivateKey string - RealityConfig reality.Config - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` + Enable bool + Listen string + Users []VlessUser + WsPath string + GrpcServiceName string + Certificate string + PrivateKey string + RealityConfig reality.Config + MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } func (t VlessServer) String() string { diff --git a/listener/config/vmess.go b/listener/config/vmess.go index 0d9e9c4a..264b772c 100644 --- a/listener/config/vmess.go +++ b/listener/config/vmess.go @@ -14,14 +14,15 @@ type VmessUser struct { } type VmessServer struct { - Enable bool - Listen string - Users []VmessUser - WsPath string - Certificate string - PrivateKey string - RealityConfig reality.Config - MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` + Enable bool + Listen string + Users []VmessUser + WsPath string + GrpcServiceName string + Certificate string + PrivateKey string + RealityConfig reality.Config + MuxOption sing.MuxOption `yaml:"mux-option" json:"mux-option,omitempty"` } func (t VmessServer) String() string { diff --git a/listener/inbound/trojan.go b/listener/inbound/trojan.go index 0c5188e4..0cf5ef41 100644 --- a/listener/inbound/trojan.go +++ b/listener/inbound/trojan.go @@ -9,13 +9,14 @@ import ( type TrojanOption struct { BaseOption - Users []TrojanUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` - SSOption TrojanSSOption `inbound:"ss-option,omitempty"` + Users []TrojanUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + GrpcServiceName string `inbound:"grpc-service-name,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` + SSOption TrojanSSOption `inbound:"ss-option,omitempty"` } type TrojanUser struct { @@ -57,14 +58,15 @@ func NewTrojan(options *TrojanOption) (*Trojan, error) { Base: base, config: options, vs: LC.TrojanServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + GrpcServiceName: options.GrpcServiceName, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), TrojanSSOption: LC.TrojanSSOption{ Enabled: options.SSOption.Enabled, Method: options.SSOption.Method, diff --git a/listener/inbound/vless.go b/listener/inbound/vless.go index 82499824..3e892cce 100644 --- a/listener/inbound/vless.go +++ b/listener/inbound/vless.go @@ -9,12 +9,13 @@ import ( type VlessOption struct { BaseOption - Users []VlessUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` + Users []VlessUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + GrpcServiceName string `inbound:"grpc-service-name,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` } type VlessUser struct { @@ -51,14 +52,15 @@ func NewVless(options *VlessOption) (*Vless, error) { Base: base, config: options, vs: LC.VlessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + GrpcServiceName: options.GrpcServiceName, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), }, }, nil } diff --git a/listener/inbound/vmess.go b/listener/inbound/vmess.go index 151e2d7a..471d26f9 100644 --- a/listener/inbound/vmess.go +++ b/listener/inbound/vmess.go @@ -9,12 +9,13 @@ import ( type VmessOption struct { BaseOption - Users []VmessUser `inbound:"users"` - WsPath string `inbound:"ws-path,omitempty"` - Certificate string `inbound:"certificate,omitempty"` - PrivateKey string `inbound:"private-key,omitempty"` - RealityConfig RealityConfig `inbound:"reality-config,omitempty"` - MuxOption MuxOption `inbound:"mux-option,omitempty"` + Users []VmessUser `inbound:"users"` + WsPath string `inbound:"ws-path,omitempty"` + GrpcServiceName string `inbound:"grpc-service-name,omitempty"` + Certificate string `inbound:"certificate,omitempty"` + PrivateKey string `inbound:"private-key,omitempty"` + RealityConfig RealityConfig `inbound:"reality-config,omitempty"` + MuxOption MuxOption `inbound:"mux-option,omitempty"` } type VmessUser struct { @@ -51,14 +52,15 @@ func NewVmess(options *VmessOption) (*Vmess, error) { Base: base, config: options, vs: LC.VmessServer{ - Enable: true, - Listen: base.RawAddress(), - Users: users, - WsPath: options.WsPath, - Certificate: options.Certificate, - PrivateKey: options.PrivateKey, - RealityConfig: options.RealityConfig.Build(), - MuxOption: options.MuxOption.Build(), + Enable: true, + Listen: base.RawAddress(), + Users: users, + WsPath: options.WsPath, + GrpcServiceName: options.GrpcServiceName, + Certificate: options.Certificate, + PrivateKey: options.PrivateKey, + RealityConfig: options.RealityConfig.Build(), + MuxOption: options.MuxOption.Build(), }, }, nil } diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index 2377ff62..ba61dc53 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -18,6 +18,7 @@ import ( "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/log" + "github.com/metacubex/mihomo/transport/gun" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" "github.com/metacubex/sing-vmess/vless" @@ -92,7 +93,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tls.Config{} var realityBuilder *reality.Builder - var httpMux *http.ServeMux + var httpHandler http.Handler if config.Certificate != "" && config.PrivateKey != "" { cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) @@ -111,17 +112,28 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if config.WsPath != "" { - httpMux = http.NewServeMux() + httpMux := http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) if err != nil { http.Error(w, err.Error(), 500) return } - sl.HandleConn(conn, tunnel) + sl.HandleConn(conn, tunnel, additions...) }) + httpHandler = httpMux tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } + if config.GrpcServiceName != "" { + httpHandler = gun.NewServerHandler(gun.ServerOption{ + ServiceName: config.GrpcServiceName, + ConnHandler: func(conn net.Conn) { + sl.HandleConn(conn, tunnel, additions...) + }, + HttpHandler: httpHandler, + }) + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + } for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -141,8 +153,8 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl.listeners = append(sl.listeners, l) go func() { - if httpMux != nil { - _ = http.Serve(l, httpMux) + if httpHandler != nil { + _ = http.Serve(l, httpHandler) return } for { diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index b344fcb4..74566eb1 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -16,6 +16,7 @@ import ( "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" "github.com/metacubex/mihomo/ntp" + "github.com/metacubex/mihomo/transport/gun" mihomoVMess "github.com/metacubex/mihomo/transport/vmess" vmess "github.com/metacubex/sing-vmess" @@ -76,7 +77,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tls.Config{} var realityBuilder *reality.Builder - var httpMux *http.ServeMux + var httpHandler http.Handler if config.Certificate != "" && config.PrivateKey != "" { cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) @@ -95,17 +96,28 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if config.WsPath != "" { - httpMux = http.NewServeMux() + httpMux := http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) if err != nil { http.Error(w, err.Error(), 500) return } - sl.HandleConn(conn, tunnel) + sl.HandleConn(conn, tunnel, additions...) }) + httpHandler = httpMux tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } + if config.GrpcServiceName != "" { + httpHandler = gun.NewServerHandler(gun.ServerOption{ + ServiceName: config.GrpcServiceName, + ConnHandler: func(conn net.Conn) { + sl.HandleConn(conn, tunnel, additions...) + }, + HttpHandler: httpHandler, + }) + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + } for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -123,8 +135,8 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) sl.listeners = append(sl.listeners, l) go func() { - if httpMux != nil { - _ = http.Serve(l, httpMux) + if httpHandler != nil { + _ = http.Serve(l, httpHandler) return } for { diff --git a/listener/trojan/server.go b/listener/trojan/server.go index 76177882..798df389 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -14,6 +14,7 @@ import ( LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" "github.com/metacubex/mihomo/listener/sing" + "github.com/metacubex/mihomo/transport/gun" "github.com/metacubex/mihomo/transport/shadowsocks/core" "github.com/metacubex/mihomo/transport/socks5" "github.com/metacubex/mihomo/transport/trojan" @@ -70,7 +71,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tls.Config{} var realityBuilder *reality.Builder - var httpMux *http.ServeMux + var httpHandler http.Handler if config.Certificate != "" && config.PrivateKey != "" { cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) @@ -89,17 +90,28 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) } } if config.WsPath != "" { - httpMux = http.NewServeMux() + httpMux := http.NewServeMux() httpMux.HandleFunc(config.WsPath, func(w http.ResponseWriter, r *http.Request) { conn, err := mihomoVMess.StreamUpgradedWebsocketConn(w, r) if err != nil { http.Error(w, err.Error(), 500) return } - sl.HandleConn(conn, tunnel) + sl.HandleConn(conn, tunnel, additions...) }) + httpHandler = httpMux tlsConfig.NextProtos = append(tlsConfig.NextProtos, "http/1.1") } + if config.GrpcServiceName != "" { + httpHandler = gun.NewServerHandler(gun.ServerOption{ + ServiceName: config.GrpcServiceName, + ConnHandler: func(conn net.Conn) { + sl.HandleConn(conn, tunnel, additions...) + }, + HttpHandler: httpHandler, + }) + tlsConfig.NextProtos = append(tlsConfig.NextProtos, "h2") + } for _, addr := range strings.Split(config.Listen, ",") { addr := addr @@ -119,8 +131,8 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) sl.listeners = append(sl.listeners, l) go func() { - if httpMux != nil { - _ = http.Serve(l, httpMux) + if httpHandler != nil { + _ = http.Serve(l, httpHandler) return } for { @@ -132,7 +144,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) continue } - go sl.HandleConn(c, tunnel) + go sl.HandleConn(c, tunnel, additions...) } }() } diff --git a/transport/gun/gun.go b/transport/gun/gun.go index cf986c8e..c28a16e8 100644 --- a/transport/gun/gun.go +++ b/transport/gun/gun.go @@ -38,15 +38,17 @@ var defaultHeader = http.Header{ type DialFn = func(network, addr string) (net.Conn, error) type Conn struct { - response *http.Response - request *http.Request - transport *TransportWrap - writer *io.PipeWriter - once sync.Once - close atomic.Bool - err error - remain int - br *bufio.Reader + initFn func() (io.ReadCloser, error) + writer io.Writer + flusher http.Flusher + netAddr + + reader io.ReadCloser + once sync.Once + close atomic.Bool + err error + remain int + br *bufio.Reader // deadlines deadline *time.Timer } @@ -57,26 +59,32 @@ type Config struct { ClientFingerprint string } -func (g *Conn) initRequest() { - response, err := g.transport.RoundTrip(g.request) +func (g *Conn) initReader() { + reader, err := g.initFn() if err != nil { g.err = err - g.writer.Close() + if closer, ok := g.writer.(io.Closer); ok { + closer.Close() + } return } if !g.close.Load() { - g.response = response - g.br = bufio.NewReader(response.Body) + g.reader = reader + g.br = bufio.NewReader(reader) } else { - response.Body.Close() + reader.Close() } } +func (g *Conn) Init() error { + g.once.Do(g.initReader) + return g.err +} + func (g *Conn) Read(b []byte) (n int, err error) { - g.once.Do(g.initRequest) - if g.err != nil { - return 0, g.err + if err = g.Init(); err != nil { + return } if g.remain > 0 { @@ -88,7 +96,7 @@ func (g *Conn) Read(b []byte) (n int, err error) { n, err = io.ReadFull(g.br, b[:size]) g.remain -= n return - } else if g.response == nil { + } else if g.reader == nil { return 0, net.ErrClosed } @@ -139,6 +147,10 @@ func (g *Conn) Write(b []byte) (n int, err error) { err = g.err } + if g.flusher != nil { + g.flusher.Flush() + } + return len(b), err } @@ -158,6 +170,10 @@ func (g *Conn) WriteBuffer(buffer *buf.Buffer) error { err = g.err } + if g.flusher != nil { + g.flusher.Flush() + } + return err } @@ -167,15 +183,16 @@ func (g *Conn) FrontHeadroom() int { func (g *Conn) Close() error { g.close.Store(true) - if r := g.response; r != nil { - r.Body.Close() + if reader := g.reader; reader != nil { + reader.Close() } - return g.writer.Close() + if closer, ok := g.writer.(io.Closer); ok { + return closer.Close() + } + return nil } -func (g *Conn) LocalAddr() net.Addr { return g.transport.LocalAddr() } -func (g *Conn) RemoteAddr() net.Addr { return g.transport.RemoteAddr() } func (g *Conn) SetReadDeadline(t time.Time) error { return g.SetDeadline(t) } func (g *Conn) SetWriteDeadline(t time.Time) error { return g.SetDeadline(t) } @@ -200,6 +217,7 @@ func NewHTTP2Client(dialFn DialFn, tlsConfig *tls.Config, Fingerprint string, re return nil, err } wrap.remoteAddr = pconn.RemoteAddr() + wrap.localAddr = pconn.LocalAddr() if tlsConfig == nil { return pconn, nil @@ -286,13 +304,18 @@ func StreamGunWithTransport(transport *TransportWrap, cfg *Config) (net.Conn, er } conn := &Conn{ - request: request, - transport: transport, - writer: writer, - close: atomic.NewBool(false), + initFn: func() (io.ReadCloser, error) { + response, err := transport.RoundTrip(request) + if err != nil { + return nil, err + } + return response.Body, nil + }, + writer: writer, + netAddr: transport.netAddr, } - go conn.once.Do(conn.initRequest) + go conn.Init() return conn, nil } diff --git a/transport/gun/server.go b/transport/gun/server.go new file mode 100644 index 00000000..ac5e59b1 --- /dev/null +++ b/transport/gun/server.go @@ -0,0 +1,70 @@ +package gun + +import ( + "io" + "net" + "net/http" + "strings" + "time" + + N "github.com/metacubex/mihomo/common/net" + C "github.com/metacubex/mihomo/constant" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" +) + +const idleTimeout = 30 * time.Second + +type ServerOption struct { + ServiceName string + ConnHandler func(conn net.Conn) + HttpHandler http.Handler +} + +func NewServerHandler(options ServerOption) http.Handler { + path := "/" + options.ServiceName + "/Tun" + connHandler := options.ConnHandler + httpHandler := options.HttpHandler + if httpHandler == nil { + httpHandler = http.NewServeMux() + } + // using h2c.NewHandler to ensure we can work in plain http2 + // and some tls conn is not *tls.Conn (like *reality.Conn) + return h2c.NewHandler(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if request.URL.Path == path && + request.Method == http.MethodPost && + strings.HasPrefix(request.Header.Get("Content-Type"), "application/grpc") { + + writer.Header().Set("Content-Type", "application/grpc") + writer.Header().Set("TE", "trailers") + writer.WriteHeader(http.StatusOK) + + conn := &Conn{ + initFn: func() (io.ReadCloser, error) { + return request.Body, nil + }, + writer: writer, + flusher: writer.(http.Flusher), + } + if request.RemoteAddr != "" { + metadata := C.Metadata{} + if err := metadata.SetRemoteAddress(request.RemoteAddr); err == nil { + conn.remoteAddr = net.TCPAddrFromAddrPort(metadata.AddrPort()) + } + } + if addr, ok := request.Context().Value(http.LocalAddrContextKey).(net.Addr); ok { + conn.localAddr = addr + } + + // gun.Conn can't correct handle ReadDeadline + // so call N.NewDeadlineConn to add a safe wrapper + connHandler(N.NewDeadlineConn(conn)) + return + } + + httpHandler.ServeHTTP(writer, request) + }), &http2.Server{ + IdleTimeout: idleTimeout, + }) +} diff --git a/transport/gun/transport.go b/transport/gun/transport.go index 12ccfd5c..c74aec46 100644 --- a/transport/gun/transport.go +++ b/transport/gun/transport.go @@ -7,8 +7,7 @@ import ( type TransportWrap struct { *http2.Transport - remoteAddr net.Addr - localAddr net.Addr + netAddr } func (tw *TransportWrap) RemoteAddr() net.Addr { @@ -18,3 +17,16 @@ func (tw *TransportWrap) RemoteAddr() net.Addr { func (tw *TransportWrap) LocalAddr() net.Addr { return tw.localAddr } + +type netAddr struct { + remoteAddr net.Addr + localAddr net.Addr +} + +func (addr *netAddr) RemoteAddr() net.Addr { + return addr.remoteAddr +} + +func (addr *netAddr) LocalAddr() net.Addr { + return addr.localAddr +}