Files
sdm-xray/stealth.patch
T

1373 lines
39 KiB
Diff
Executable File

diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go
index 24b5fba..2faae9c 100644
--- a/infra/conf/transport_internet.go
+++ b/infra/conf/transport_internet.go
@@ -36,6 +36,8 @@ func (p TransportProtocol) Build() (string, error) {
return "", errors.PrintRemovedFeatureError("QUIC transport (without web service, etc.)", "XHTTP stream-one H3")
case "hysteria":
return "hysteria", nil
+ case "stealth":
+ return "stealth", nil
default:
return "", errors.New("Config: unknown transport protocol: ", p)
}
@@ -59,6 +61,7 @@ type StreamConfig struct {
WSSettings *WebSocketConfig `json:"wsSettings"`
HTTPUPGRADESettings *HttpUpgradeConfig `json:"httpupgradeSettings"`
HysteriaSettings *HysteriaConfig `json:"hysteriaSettings"`
+ StealthSettings *StealthConfig `json:"stealthSettings"`
SocketSettings *SocketConfig `json:"sockopt"`
}
@@ -97,8 +100,8 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
config.SecuritySettings = append(config.SecuritySettings, tm)
config.SecurityType = tm.Type
case "reality":
- if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" {
- return nil, errors.New("REALITY only supports RAW, XHTTP and gRPC for now.")
+ if config.ProtocolName != "tcp" && config.ProtocolName != "splithttp" && config.ProtocolName != "grpc" && config.ProtocolName != "stealth" {
+ return nil, errors.New("REALITY only supports RAW, XHTTP, gRPC and stealth for now.")
}
if c.REALITYSettings == nil {
return nil, errors.New(`REALITY: Empty "realitySettings".`)
@@ -142,6 +145,16 @@ func (c *StreamConfig) Build() (*internet.StreamConfig, error) {
Settings: serial.ToTypedMessage(hs),
})
}
+ if c.StealthSettings != nil {
+ hs, err := c.StealthSettings.Build()
+ if err != nil {
+ return nil, errors.New("Failed to build stealth config.").Base(err)
+ }
+ config.TransportSettings = append(config.TransportSettings, &internet.TransportConfig{
+ ProtocolName: "stealth",
+ Settings: serial.ToTypedMessage(hs),
+ })
+ }
if c.KCPSettings != nil {
ts, err := c.KCPSettings.Build()
if err != nil {
diff --git a/infra/conf/transport_method.go b/infra/conf/transport_method.go
index 19cb496..d6db814 100644
--- a/infra/conf/transport_method.go
+++ b/infra/conf/transport_method.go
@@ -22,6 +22,7 @@ import (
"github.com/xtls/xray-core/transport/internet/hysteria"
"github.com/xtls/xray-core/transport/internet/kcp"
"github.com/xtls/xray-core/transport/internet/splithttp"
+ "github.com/xtls/xray-core/transport/internet/stealth"
"github.com/xtls/xray-core/transport/internet/tcp"
"github.com/xtls/xray-core/transport/internet/websocket"
"google.golang.org/protobuf/proto"
@@ -812,3 +813,44 @@ func readFileOrString(f string, s []string) ([]byte, error) {
}
return nil, errors.New("both file and bytes are empty.")
}
+
+
+// StealthConfig configures the stealth transport.
+type StealthConfig struct {
+ Key string `json:"key"`
+ PadMax int64 `json:"padMax"`
+ PadProb int64 `json:"padProb"`
+ PaceMs int64 `json:"paceMs"`
+ PaceJitterMs int64 `json:"paceJitterMs"`
+ Path string `json:"path"`
+ Paths []string `json:"paths"`
+ Ports []uint32 `json:"ports"`
+ Bps int64 `json:"bps"`
+ Target string `json:"target"`
+ Decoy bool `json:"decoy"`
+ DecoyEveryS int64 `json:"decoyEverySecs"`
+ DecoyTarget string `json:"decoyTarget"`
+ PadProfile string `json:"padProfile"`
+ Retries int64 `json:"retries"`
+}
+
+// Build implements Buildable.
+func (c *StealthConfig) Build() (proto.Message, error) {
+ return &stealth.Config{
+ Key: c.Key,
+ PadMax: c.PadMax,
+ PadProb: c.PadProb,
+ PaceMs: c.PaceMs,
+ PaceJitterMs: c.PaceJitterMs,
+ Path: c.Path,
+ Paths: c.Paths,
+ Ports: c.Ports,
+ Bps: c.Bps,
+ Target: c.Target,
+ Decoy: c.Decoy,
+ DecoyTarget: c.DecoyTarget,
+ DecoyEverySecs: c.DecoyEveryS,
+ PadProfile: c.PadProfile,
+ Retries: c.Retries,
+ }, nil
+}
diff --git a/transport/internet/stealth/config.go b/transport/internet/stealth/config.go
new file mode 100644
index 0000000..396a04a
--- /dev/null
+++ b/transport/internet/stealth/config.go
@@ -0,0 +1,175 @@
+package stealth
+
+import (
+ "net/url"
+ "strings"
+ "time"
+
+ "github.com/xtls/xray-core/common"
+ "github.com/xtls/xray-core/transport/internet"
+)
+
+const protocolName = "stealth"
+
+const authOK = "ok"
+
+const chromeUA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36"
+
+func init() {
+ common.Must(internet.RegisterProtocolConfigCreator(protocolName, func() interface{} {
+ return new(Config)
+ }))
+}
+
+
+func (c *Config) GetNormalizedPaths() []string {
+ if len(c.Paths) > 0 {
+ out := make([]string, 0, len(c.Paths))
+ for _, p := range c.Paths {
+ if !strings.HasPrefix(p, "/") {
+ p = "/" + p
+ }
+ out = append(out, p)
+ }
+ return out
+ }
+ return []string{c.GetNormalizedPath()}
+}
+
+func (c *Config) NormalizedPadProb() int {
+ if c.PadProb <= 0 {
+ return 50
+ }
+ if c.PadProb > 100 {
+ return 100
+ }
+ return int(c.PadProb)
+}
+
+
+func (c *Config) GetNormalizedPorts() []uint32 {
+ if len(c.Ports) == 0 {
+ return nil
+ }
+ return c.Ports
+}
+
+func (c *Config) NormalizedBps() int {
+ if c.Bps <= 0 {
+ return 0
+ }
+ if c.Bps > 1<<30 {
+ return 1 << 30
+ }
+ return int(c.Bps)
+}
+
+
+
+func (c *Config) DecoyEnabled() bool {
+ return c.Decoy
+}
+
+func (c *Config) DecoyEvery() time.Duration {
+ n := c.DecoyEverySecs
+ if n <= 0 {
+ n = 60
+ }
+ if n > 86400 {
+ n = 86400
+ }
+ return time.Duration(n) * time.Second
+}
+
+func (c *Config) NormalizedDecoyTarget() string {
+ t := strings.TrimSpace(c.DecoyTarget)
+ if t == "" {
+ return "https://www.cloudflare.com/cdn-cgi/trace"
+ }
+ if !strings.Contains(t, "://") {
+ t = "https://" + t
+ }
+ return t
+}
+
+func (c *Config) GetNormalizedTarget() *url.URL {
+ t := strings.TrimSpace(c.Target)
+ if t == "" {
+ return nil
+ }
+ if !strings.Contains(t, "://") {
+ t = "https://" + t
+ }
+ u, err := url.Parse(t)
+ if err != nil || u.Host == "" {
+ return nil
+ }
+ return u
+}
+
+func (c *Config) GetNormalizedPath() string {
+ path := c.Path
+ if path == "" {
+ return "/"
+ }
+ if !strings.HasPrefix(path, "/") {
+ path = "/" + path
+ }
+ return path
+}
+
+
+
+func (c *Config) GetNormalizedRetries() int {
+ n := c.Retries
+ if n == 0 {
+ return 3
+ }
+ if n < 0 {
+ return 0
+ }
+ if n > 10 {
+ return 10
+ }
+ return int(n)
+}
+
+func (c *Config) NormalizedPadProfile() string {
+ switch c.PadProfile {
+ case "uniform":
+ return "uniform"
+ default:
+ return "browser"
+ }
+}
+
+func (c *Config) NormalizedPadMax() int {
+ if c.PadMax <= 0 {
+ return 0
+ }
+ if c.PadMax > 4096 {
+ return 4096
+ }
+ return int(c.PadMax)
+}
+
+
+func (c *Config) NormalizedPaceJitterMs() time.Duration {
+ if c.PaceJitterMs <= 0 {
+ return 0
+ }
+ if c.PaceJitterMs > 5000 {
+ return 5000
+ }
+ return time.Duration(c.PaceJitterMs) * time.Millisecond
+}
+
+func (c *Config) NormalizedPaceMs() time.Duration {
+ if c.PaceMs <= 0 {
+ return 0
+ }
+ if c.PaceMs > 5000 {
+ return 5000
+ }
+ return time.Duration(c.PaceMs) * time.Millisecond
+}
diff --git a/transport/internet/stealth/config.pb.go b/transport/internet/stealth/config.pb.go
new file mode 100644
index 0000000..289aa60
--- /dev/null
+++ b/transport/internet/stealth/config.pb.go
@@ -0,0 +1,268 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.12
+// protoc v3.21.12
+// source: transport/internet/stealth/config.proto
+
+package stealth
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type Config struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ // Shared auth secret (both sides must match).
+ Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
+ // Max random padding bytes appended after each data frame (0 = disabled).
+ PadMax int64 `protobuf:"varint,2,opt,name=pad_max,json=padMax,proto3" json:"pad_max,omitempty"`
+ // Minimum interval between frame writes in ms (0 = disabled).
+ PaceMs int64 `protobuf:"varint,3,opt,name=pace_ms,json=paceMs,proto3" json:"pace_ms,omitempty"`
+ // HTTP/2 request path (default "/").
+ Path string `protobuf:"bytes,4,opt,name=path,proto3" json:"path,omitempty"`
+ // Max random extra delay added to pacing (ms, 0 = disabled).
+ PaceJitterMs int64 `protobuf:"varint,5,opt,name=pace_jitter_ms,json=paceJitterMs,proto3" json:"pace_jitter_ms,omitempty"`
+ // Multiple tunnel paths for identity rotation; falls back to [path].
+ Paths []string `protobuf:"bytes,6,rep,name=paths,proto3" json:"paths,omitempty"`
+ // Percent chance (0-100) to append a PAD frame after a data frame (default 50).
+ PadProb int64 `protobuf:"varint,7,opt,name=pad_prob,json=padProb,proto3" json:"pad_prob,omitempty"`
+ // Candidate ports for identity rotation (client picks one per connection).
+ // Overrides the outbound port; the server must listen on these ports.
+ Ports []uint32 `protobuf:"varint,8,rep,packed,name=ports,proto3" json:"ports,omitempty"`
+ // Optional upload rate limit in bytes/sec (0 = unlimited).
+ Bps int64 `protobuf:"varint,9,opt,name=bps,proto3" json:"bps,omitempty"`
+ // Reverse-proxy target for non-tunnel requests (a real website to mirror).
+ // Empty = serve the built-in landing page only.
+ Target string `protobuf:"bytes,10,opt,name=target,proto3" json:"target,omitempty"`
+ // Client-side decoy: periodically make a real request to a benign CDN
+ // (direct, not through the tunnel) to mask aggregated activity. Default off.
+ Decoy bool `protobuf:"varint,11,opt,name=decoy,proto3" json:"decoy,omitempty"`
+ // Decoy interval in seconds (default 60).
+ DecoyEverySecs int64 `protobuf:"varint,12,opt,name=decoy_every_secs,json=decoyEverySecs,proto3" json:"decoy_every_secs,omitempty"`
+ // Decoy target URL (default Cloudflare trace endpoint).
+ DecoyTarget string `protobuf:"bytes,13,opt,name=decoy_target,json=decoyTarget,proto3" json:"decoy_target,omitempty"`
+ // Padding profile: "browser" (default, biased toward small pads) or "uniform".
+ PadProfile string `protobuf:"bytes,14,opt,name=pad_profile,json=padProfile,proto3" json:"pad_profile,omitempty"`
+ // Dial retries with a fresh identity (new path/port) on failure (default 3).
+ Retries int64 `protobuf:"varint,15,opt,name=retries,proto3" json:"retries,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Config) Reset() {
+ *x = Config{}
+ mi := &file_transport_internet_stealth_config_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Config) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Config) ProtoMessage() {}
+
+func (x *Config) ProtoReflect() protoreflect.Message {
+ mi := &file_transport_internet_stealth_config_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Config.ProtoReflect.Descriptor instead.
+func (*Config) Descriptor() ([]byte, []int) {
+ return file_transport_internet_stealth_config_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Config) GetKey() string {
+ if x != nil {
+ return x.Key
+ }
+ return ""
+}
+
+func (x *Config) GetPadMax() int64 {
+ if x != nil {
+ return x.PadMax
+ }
+ return 0
+}
+
+func (x *Config) GetPaceMs() int64 {
+ if x != nil {
+ return x.PaceMs
+ }
+ return 0
+}
+
+func (x *Config) GetPath() string {
+ if x != nil {
+ return x.Path
+ }
+ return ""
+}
+
+func (x *Config) GetPaceJitterMs() int64 {
+ if x != nil {
+ return x.PaceJitterMs
+ }
+ return 0
+}
+
+func (x *Config) GetPaths() []string {
+ if x != nil {
+ return x.Paths
+ }
+ return nil
+}
+
+func (x *Config) GetPadProb() int64 {
+ if x != nil {
+ return x.PadProb
+ }
+ return 0
+}
+
+func (x *Config) GetPorts() []uint32 {
+ if x != nil {
+ return x.Ports
+ }
+ return nil
+}
+
+func (x *Config) GetBps() int64 {
+ if x != nil {
+ return x.Bps
+ }
+ return 0
+}
+
+func (x *Config) GetTarget() string {
+ if x != nil {
+ return x.Target
+ }
+ return ""
+}
+
+func (x *Config) GetDecoy() bool {
+ if x != nil {
+ return x.Decoy
+ }
+ return false
+}
+
+func (x *Config) GetDecoyEverySecs() int64 {
+ if x != nil {
+ return x.DecoyEverySecs
+ }
+ return 0
+}
+
+func (x *Config) GetDecoyTarget() string {
+ if x != nil {
+ return x.DecoyTarget
+ }
+ return ""
+}
+
+func (x *Config) GetPadProfile() string {
+ if x != nil {
+ return x.PadProfile
+ }
+ return ""
+}
+
+func (x *Config) GetRetries() int64 {
+ if x != nil {
+ return x.Retries
+ }
+ return 0
+}
+
+var File_transport_internet_stealth_config_proto protoreflect.FileDescriptor
+
+const file_transport_internet_stealth_config_proto_rawDesc = "" +
+ "\n" +
+ "'transport/internet/stealth/config.proto\x12\x1fxray.transport.internet.stealth\"\x95\x03\n" +
+ "\x06Config\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x17\n" +
+ "\apad_max\x18\x02 \x01(\x03R\x06padMax\x12\x17\n" +
+ "\apace_ms\x18\x03 \x01(\x03R\x06paceMs\x12\x12\n" +
+ "\x04path\x18\x04 \x01(\tR\x04path\x12$\n" +
+ "\x0epace_jitter_ms\x18\x05 \x01(\x03R\fpaceJitterMs\x12\x14\n" +
+ "\x05paths\x18\x06 \x03(\tR\x05paths\x12\x19\n" +
+ "\bpad_prob\x18\a \x01(\x03R\apadProb\x12\x14\n" +
+ "\x05ports\x18\b \x03(\rR\x05ports\x12\x10\n" +
+ "\x03bps\x18\t \x01(\x03R\x03bps\x12\x16\n" +
+ "\x06target\x18\n" +
+ " \x01(\tR\x06target\x12\x14\n" +
+ "\x05decoy\x18\v \x01(\bR\x05decoy\x12(\n" +
+ "\x10decoy_every_secs\x18\f \x01(\x03R\x0edecoyEverySecs\x12!\n" +
+ "\fdecoy_target\x18\r \x01(\tR\vdecoyTarget\x12\x1f\n" +
+ "\vpad_profile\x18\x0e \x01(\tR\n" +
+ "padProfile\x12\x18\n" +
+ "\aretries\x18\x0f \x01(\x03R\aretriesB6Z4github.com/xtls/xray-core/transport/internet/stealthb\x06proto3"
+
+var (
+ file_transport_internet_stealth_config_proto_rawDescOnce sync.Once
+ file_transport_internet_stealth_config_proto_rawDescData []byte
+)
+
+func file_transport_internet_stealth_config_proto_rawDescGZIP() []byte {
+ file_transport_internet_stealth_config_proto_rawDescOnce.Do(func() {
+ file_transport_internet_stealth_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_transport_internet_stealth_config_proto_rawDesc), len(file_transport_internet_stealth_config_proto_rawDesc)))
+ })
+ return file_transport_internet_stealth_config_proto_rawDescData
+}
+
+var file_transport_internet_stealth_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_transport_internet_stealth_config_proto_goTypes = []any{
+ (*Config)(nil), // 0: xray.transport.internet.stealth.Config
+}
+var file_transport_internet_stealth_config_proto_depIdxs = []int32{
+ 0, // [0:0] is the sub-list for method output_type
+ 0, // [0:0] is the sub-list for method input_type
+ 0, // [0:0] is the sub-list for extension type_name
+ 0, // [0:0] is the sub-list for extension extendee
+ 0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_transport_internet_stealth_config_proto_init() }
+func file_transport_internet_stealth_config_proto_init() {
+ if File_transport_internet_stealth_config_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_transport_internet_stealth_config_proto_rawDesc), len(file_transport_internet_stealth_config_proto_rawDesc)),
+ NumEnums: 0,
+ NumMessages: 1,
+ NumExtensions: 0,
+ NumServices: 0,
+ },
+ GoTypes: file_transport_internet_stealth_config_proto_goTypes,
+ DependencyIndexes: file_transport_internet_stealth_config_proto_depIdxs,
+ MessageInfos: file_transport_internet_stealth_config_proto_msgTypes,
+ }.Build()
+ File_transport_internet_stealth_config_proto = out.File
+ file_transport_internet_stealth_config_proto_goTypes = nil
+ file_transport_internet_stealth_config_proto_depIdxs = nil
+}
diff --git a/transport/internet/stealth/config.proto b/transport/internet/stealth/config.proto
new file mode 100644
index 0000000..e367189
--- /dev/null
+++ b/transport/internet/stealth/config.proto
@@ -0,0 +1,40 @@
+syntax = "proto3";
+
+package xray.transport.internet.stealth;
+option go_package = "github.com/xtls/xray-core/transport/internet/stealth";
+
+message Config {
+ // Shared auth secret (both sides must match).
+ string key = 1;
+ // Max random padding bytes appended after each data frame (0 = disabled).
+ int64 pad_max = 2;
+ // Minimum interval between frame writes in ms (0 = disabled).
+ int64 pace_ms = 3;
+ // HTTP/2 request path (default "/").
+ string path = 4;
+ // Max random extra delay added to pacing (ms, 0 = disabled).
+ int64 pace_jitter_ms = 5;
+ // Multiple tunnel paths for identity rotation; falls back to [path].
+ repeated string paths = 6;
+ // Percent chance (0-100) to append a PAD frame after a data frame (default 50).
+ int64 pad_prob = 7;
+ // Candidate ports for identity rotation (client picks one per connection).
+ // Overrides the outbound port; the server must listen on these ports.
+ repeated uint32 ports = 8;
+ // Optional upload rate limit in bytes/sec (0 = unlimited).
+ int64 bps = 9;
+ // Reverse-proxy target for non-tunnel requests (a real website to mirror).
+ // Empty = serve the built-in landing page only.
+ string target = 10;
+ // Client-side decoy: periodically make a real request to a benign CDN
+ // (direct, not through the tunnel) to mask aggregated activity. Default off.
+ bool decoy = 11;
+ // Decoy interval in seconds (default 60).
+ int64 decoy_every_secs = 12;
+ // Decoy target URL (default Cloudflare trace endpoint).
+ string decoy_target = 13;
+ // Padding profile: "browser" (default, biased toward small pads) or "uniform".
+ string pad_profile = 14;
+ // Dial retries with a fresh identity (new path/port) on failure (default 3).
+ int64 retries = 15;
+}
diff --git a/transport/internet/stealth/conn.go b/transport/internet/stealth/conn.go
new file mode 100644
index 0000000..88fb99d
--- /dev/null
+++ b/transport/internet/stealth/conn.go
@@ -0,0 +1,160 @@
+package stealth
+
+import (
+ "context"
+ "crypto/rand"
+ "io"
+ mathrand "math/rand"
+ "net"
+ "sync"
+ "time"
+
+ "golang.org/x/time/rate"
+)
+
+type h2Conn struct {
+ reader io.Reader
+ writer io.Writer
+ cancel context.CancelFunc
+ flush func()
+ done chan struct{}
+ padMax int
+ padProb int
+ padUni bool
+ paceMs time.Duration
+ paceJit time.Duration
+ limiter *rate.Limiter
+ mu sync.Mutex
+ lastWrite time.Time
+ pending []byte
+ localAddr net.Addr
+ remote net.Addr
+}
+
+func newH2Conn(reader io.Reader, writer io.Writer, cancel context.CancelFunc, flush func(), cfg *Config, local, remote net.Addr) *h2Conn {
+ return &h2Conn{
+ reader: reader,
+ writer: writer,
+ cancel: cancel,
+ flush: flush,
+ done: make(chan struct{}),
+ padMax: cfg.NormalizedPadMax(),
+ padProb: cfg.NormalizedPadProb(),
+ padUni: cfg.NormalizedPadProfile() == "uniform",
+ paceMs: cfg.NormalizedPaceMs(),
+ paceJit: cfg.NormalizedPaceJitterMs(),
+ limiter: newLimiter(cfg),
+ localAddr: local,
+ remote: remote,
+ }
+}
+
+func newLimiter(cfg *Config) *rate.Limiter {
+ if bps := cfg.NormalizedBps(); bps > 0 {
+ return rate.NewLimiter(rate.Limit(bps), bps)
+ }
+ return nil
+}
+
+// padSize returns a browser-like (mostly small, occasional large) or uniform size.
+func (c *h2Conn) padSize() int {
+ if c.padMax <= 0 {
+ return 0
+ }
+ if c.padUni {
+ return mathrand.Intn(c.padMax + 1)
+ }
+ if mathrand.Intn(100) < 80 {
+ return mathrand.Intn(c.padMax/8 + 1)
+ }
+ return mathrand.Intn(c.padMax + 1)
+}
+
+func (c *h2Conn) Read(p []byte) (int, error) {
+ for {
+ if len(c.pending) > 0 {
+ n := copy(p, c.pending)
+ c.pending = c.pending[n:]
+ return n, nil
+ }
+ ftype, payload, err := readFrame(c.reader)
+ if err != nil {
+ return 0, err
+ }
+ switch ftype {
+ case frameTypeData:
+ n := copy(p, payload)
+ if n < len(payload) {
+ c.pending = append(c.pending[:0], payload[n:]...)
+ }
+ return n, nil
+ case frameTypeClose:
+ return 0, io.EOF
+ default:
+ // padding and unknown frames are skipped
+ }
+ }
+}
+
+func (c *h2Conn) Write(p []byte) (int, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ select {
+ case <-c.done:
+ return 0, net.ErrClosed
+ default:
+ }
+ if c.limiter != nil {
+ if err := c.limiter.WaitN(context.Background(), len(p)); err != nil {
+ return 0, err
+ }
+ }
+ if c.paceMs > 0 {
+ d := c.lastWrite.Add(c.paceMs).Sub(time.Now())
+ if c.paceJit > 0 {
+ d += time.Duration(mathrand.Int63n(int64(c.paceJit)))
+ }
+ if d > 0 {
+ time.Sleep(d)
+ }
+ }
+ if err := writeFrame(c.writer, frameTypeData, p); err != nil {
+ return 0, err
+ }
+ if c.padMax > 0 && mathrand.Intn(100) < c.padProb {
+ pad := make([]byte, c.padSize())
+ _, _ = rand.Read(pad)
+ if err := writeFrame(c.writer, frameTypePad, pad); err != nil {
+ return 0, err
+ }
+ }
+ if c.flush != nil {
+ c.flush()
+ }
+ c.lastWrite = time.Now()
+ return len(p), nil
+}
+
+func (c *h2Conn) Close() error {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ select {
+ case <-c.done:
+ return nil
+ default:
+ close(c.done)
+ }
+ if c.cancel != nil {
+ c.cancel()
+ }
+ return nil
+}
+
+// Done returns a channel closed when the connection is closed.
+func (c *h2Conn) Done() <-chan struct{} { return c.done }
+
+func (c *h2Conn) LocalAddr() net.Addr { return c.localAddr }
+func (c *h2Conn) RemoteAddr() net.Addr { return c.remote }
+func (c *h2Conn) SetDeadline(t time.Time) error { return nil }
+func (c *h2Conn) SetReadDeadline(t time.Time) error { return nil }
+func (c *h2Conn) SetWriteDeadline(t time.Time) error { return nil }
diff --git a/transport/internet/stealth/dialer.go b/transport/internet/stealth/dialer.go
new file mode 100644
index 0000000..50a6e38
--- /dev/null
+++ b/transport/internet/stealth/dialer.go
@@ -0,0 +1,205 @@
+package stealth
+
+import (
+ "context"
+ gotls "crypto/tls"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httptrace"
+ mathrand "math/rand"
+ "sync"
+ "time"
+
+ "github.com/xtls/xray-core/common"
+ "github.com/xtls/xray-core/common/errors"
+ xnet "github.com/xtls/xray-core/common/net"
+ "github.com/xtls/xray-core/transport/internet"
+ "github.com/xtls/xray-core/transport/internet/reality"
+ "github.com/xtls/xray-core/transport/internet/stat"
+ "github.com/xtls/xray-core/transport/internet/tls"
+ "golang.org/x/net/http2"
+)
+
+var (
+ clientCache = make(map[string]*http.Client)
+ clientCacheMu sync.Mutex
+ clientCacheAt = make(map[string]time.Time)
+
+ decoyOnce sync.Once
+)
+
+func dialTLS(ctx context.Context, dest xnet.Destination, streamSettings *internet.MemoryStreamConfig) (net.Conn, error) {
+ conn, err := internet.DialSystem(ctx, dest, streamSettings.SocketSettings)
+ if err != nil {
+ return nil, err
+ }
+ if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
+ return reality.UClient(conn, config, ctx, dest)
+ }
+ if config := tls.ConfigFromStreamSettings(streamSettings); config != nil {
+ tlsConfig := config.GetTLSConfig(tls.WithDestination(dest))
+ if fingerprint := tls.GetFingerprint(config.Fingerprint); fingerprint != nil {
+ uconn := tls.UClient(conn, tlsConfig, fingerprint)
+ if err := uconn.(*tls.UConn).HandshakeContext(ctx); err != nil {
+ conn.Close()
+ return nil, errors.New("stealth: TLS handshake failed").Base(err)
+ }
+ conn = uconn
+ } else {
+ tconn := tls.Client(conn, tlsConfig)
+ if err := tconn.(*tls.Conn).HandshakeContext(ctx); err != nil {
+ conn.Close()
+ return nil, errors.New("stealth: TLS handshake failed").Base(err)
+ }
+ conn = tconn
+ }
+ }
+ return conn, nil
+}
+
+func getHTTPClient(dest xnet.Destination, streamSettings *internet.MemoryStreamConfig) *http.Client {
+ cfg := streamSettings.ProtocolSettings.(*Config)
+ fingerprint := ""
+ if tc := tls.ConfigFromStreamSettings(streamSettings); tc != nil {
+ fingerprint = tc.Fingerprint
+ }
+ key := dest.NetAddr() + "|" + cfg.Key + "|" + cfg.GetNormalizedPath() + "|" + fingerprint
+
+ clientCacheMu.Lock()
+ defer clientCacheMu.Unlock()
+ if c, ok := clientCache[key]; ok {
+ return c
+ }
+ transport := &http2.Transport{
+ DialTLSContext: func(ctxInner context.Context, network string, addr string, tlsCfg *gotls.Config) (net.Conn, error) {
+ return dialTLS(ctxInner, dest, streamSettings)
+ },
+ IdleConnTimeout: xnet.ConnIdleTimeout,
+ }
+ client := &http.Client{Transport: transport}
+ clientCache[key] = client
+ clientCacheAt[key] = time.Now()
+ return client
+}
+
+func openStream(ctx context.Context, dest xnet.Destination, streamSettings *internet.MemoryStreamConfig, client *http.Client) (stat.Connection, error) {
+ cfg := streamSettings.ProtocolSettings.(*Config)
+ paths := cfg.GetNormalizedPaths()
+ path := paths[mathrand.Intn(len(paths))]
+
+ streamCtx, cancel := context.WithCancel(ctx)
+ pr, pw := io.Pipe()
+
+ var localAddr, remoteAddr net.Addr
+ streamCtx = httptrace.WithClientTrace(streamCtx, &httptrace.ClientTrace{
+ GotConn: func(ci httptrace.GotConnInfo) {
+ if ci.Conn != nil {
+ localAddr = ci.Conn.LocalAddr()
+ remoteAddr = ci.Conn.RemoteAddr()
+ }
+ },
+ })
+
+ target := "https://" + dest.NetAddr() + path
+ req, err := http.NewRequestWithContext(streamCtx, http.MethodPost, target, pr)
+ if err != nil {
+ cancel()
+ pw.Close()
+ return nil, err
+ }
+ req.Header.Set("Content-Type", "application/octet-stream")
+ req.Header.Set("User-Agent", chromeUA)
+ req.ContentLength = -1
+
+ // the auth frame must reach the server before it replies 200
+ go func() {
+ _ = writeFrame(pw, frameTypeAuth, []byte(cfg.Key))
+ }()
+
+ resp, err := client.Do(req)
+ if err != nil {
+ cancel()
+ pw.Close()
+ return nil, errors.New("stealth: request failed").Base(err)
+ }
+ if resp.StatusCode != http.StatusOK {
+ _, _ = io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+ pw.Close()
+ cancel()
+ return nil, fmt.Errorf("stealth: auth rejected (status %d)", resp.StatusCode)
+ }
+
+ if localAddr == nil {
+ localAddr = &net.TCPAddr{IP: net.IPv4zero}
+ }
+ if remoteAddr == nil {
+ remoteAddr = localAddr
+ }
+ cancelStream := func() {
+ pw.Close()
+ cancel()
+ }
+ c := newH2Conn(resp.Body, pw, cancelStream, nil, cfg, localAddr, remoteAddr)
+ return stat.Connection(c), nil
+}
+
+func startDecoy(cfg *Config) {
+ decoyOnce.Do(func() {
+ if !cfg.DecoyEnabled() {
+ return
+ }
+ interval := cfg.DecoyEvery()
+ target := cfg.NormalizedDecoyTarget()
+ client := &http.Client{Timeout: 15 * time.Second}
+ go func() {
+ for {
+ time.Sleep(interval)
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, target, nil)
+ if err == nil {
+ req.Header.Set("User-Agent", chromeUA)
+ resp, err := client.Do(req)
+ if err == nil {
+ _, _ = io.Copy(io.Discard, resp.Body)
+ resp.Body.Close()
+ }
+ }
+ cancel()
+ }
+ }()
+ })
+}
+
+func Dial(ctx context.Context, dest xnet.Destination, streamSettings *internet.MemoryStreamConfig) (stat.Connection, error) {
+ cfg, _ := streamSettings.ProtocolSettings.(*Config)
+ retries := 3
+ if cfg != nil {
+ retries = cfg.GetNormalizedRetries()
+ startDecoy(cfg)
+ }
+ var lastErr error
+ for attempt := 0; attempt <= retries; attempt++ {
+ effDest := dest
+ if cfg != nil {
+ if ports := cfg.GetNormalizedPorts(); len(ports) > 0 {
+ effDest.Port = xnet.Port(ports[mathrand.Intn(len(ports))])
+ }
+ }
+ errors.LogInfo(ctx, "stealth dialing to ", effDest, " (attempt ", attempt+1, ")")
+ client := getHTTPClient(effDest, streamSettings)
+ conn, err := openStream(ctx, effDest, streamSettings, client)
+ if err == nil {
+ return conn, nil
+ }
+ lastErr = err
+ errors.LogInfo(ctx, "stealth attempt failed, retrying with a new identity: ", err)
+ }
+ return nil, lastErr
+}
+
+func init() {
+ common.Must(internet.RegisterTransportDialer(protocolName, Dial))
+}
diff --git a/transport/internet/stealth/framing.go b/transport/internet/stealth/framing.go
new file mode 100644
index 0000000..dbf572b
--- /dev/null
+++ b/transport/internet/stealth/framing.go
@@ -0,0 +1,59 @@
+package stealth
+
+import (
+ "encoding/binary"
+ "errors"
+ "io"
+)
+
+const (
+ frameTypeData byte = 0x01
+ frameTypePad byte = 0x02
+ frameTypeAuth byte = 0xFE
+ frameTypeClose byte = 0xFF
+)
+
+const maxFrameSize = 64 * 1024
+
+func writeFrame(w io.Writer, ftype byte, payload []byte) error {
+ n := 1 + len(payload)
+ if n > maxFrameSize {
+ return errors.New("stealth: frame too large")
+ }
+ var hdr [4]byte
+ binary.BigEndian.PutUint32(hdr[:], uint32(n))
+ if _, err := w.Write(hdr[:]); err != nil {
+ return err
+ }
+ if _, err := w.Write([]byte{ftype}); err != nil {
+ return err
+ }
+ if len(payload) > 0 {
+ if _, err := w.Write(payload); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func readFrame(r io.Reader) (byte, []byte, error) {
+ var hdr [4]byte
+ if _, err := io.ReadFull(r, hdr[:]); err != nil {
+ return 0, nil, err
+ }
+ n := binary.BigEndian.Uint32(hdr[:])
+ if n == 0 || n > maxFrameSize {
+ return 0, nil, errors.New("stealth: bad frame length")
+ }
+ var ftype [1]byte
+ if _, err := io.ReadFull(r, ftype[:]); err != nil {
+ return 0, nil, err
+ }
+ payload := make([]byte, n-1)
+ if len(payload) > 0 {
+ if _, err := io.ReadFull(r, payload); err != nil {
+ return 0, nil, err
+ }
+ }
+ return ftype[0], payload, nil
+}
diff --git a/transport/internet/stealth/hub.go b/transport/internet/stealth/hub.go
new file mode 100644
index 0000000..08c38ac
--- /dev/null
+++ b/transport/internet/stealth/hub.go
@@ -0,0 +1,317 @@
+package stealth
+
+import (
+ "bytes"
+ "context"
+ gotls "crypto/tls"
+ "io"
+ "net"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ goreality "github.com/xtls/reality"
+ "github.com/xtls/xray-core/common"
+ "github.com/xtls/xray-core/common/errors"
+ xnet "github.com/xtls/xray-core/common/net"
+ "github.com/xtls/xray-core/transport/internet"
+ "github.com/xtls/xray-core/transport/internet/reality"
+ "github.com/xtls/xray-core/transport/internet/stat"
+ "github.com/xtls/xray-core/transport/internet/tls"
+ "golang.org/x/net/http2"
+)
+
+const landingPage = `<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>dedikar.online</title>
+<style>
+ body { font-family: -apple-system, Segoe UI, Roboto, sans-serif; margin: 0; background: #f6f7f9; color: #222; }
+ .wrap { max-width: 720px; margin: 0 auto; padding: 64px 24px; }
+ h1 { font-size: 28px; margin-bottom: 8px; }
+ p { line-height: 1.6; color: #444; }
+ .card { background: #fff; border: 1px solid #e3e5e8; border-radius: 10px; padding: 24px; margin-top: 24px; }
+ .muted { color: #888; font-size: 13px; }
+</style>
+</head>
+<body>
+<div class="wrap">
+ <h1>dedikar.online</h1>
+ <p>Personal infrastructure and utility services.</p>
+ <div class="card">
+ <p>Status: operational.</p>
+ <p class="muted">This is a private host. If you reached this page by accident, nothing to see here.</p>
+ </div>
+</div>
+</body>
+</html>
+`
+
+type mirrorHostKey struct{}
+
+
+var urlAttrRe = regexp.MustCompile(`(?i)(href|src|action|poster|data-src|data-lazy-src|srcset|content)\s*=\s*("([^"]*)"|'([^']*)')`)
+
+func isCSSResponse(resp *http.Response) bool {
+ return strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/css")
+}
+
+func isHTMLResponse(resp *http.Response) bool {
+ return strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html")
+}
+
+func rewriteURLRef(v, targetHost, ourHost string) string {
+ v = strings.ReplaceAll(v, "https://"+targetHost, "https://"+ourHost)
+ v = strings.ReplaceAll(v, "http://"+targetHost, "https://"+ourHost)
+ v = strings.ReplaceAll(v, "//"+targetHost, "//"+ourHost)
+ return v
+}
+
+func rewriteHTMLRefs(body []byte, targetHost, ourHost string) []byte {
+ if targetHost == "" || ourHost == "" || strings.EqualFold(targetHost, ourHost) {
+ return body
+ }
+ return urlAttrRe.ReplaceAllFunc(body, func(m []byte) []byte {
+ sm := urlAttrRe.FindSubmatch(m)
+ var val []byte
+ if len(sm) > 3 && sm[3] != nil {
+ val = sm[3]
+ } else if len(sm) > 4 && sm[4] != nil {
+ val = sm[4]
+ }
+ if val == nil {
+ return m
+ }
+ nv := []byte(rewriteURLRef(string(val), targetHost, ourHost))
+ return bytes.Replace(m, val, nv, 1)
+ })
+}
+
+var cssURLRe = regexp.MustCompile(`(?i)url\s*\(\s*(?:'([^']*)'|"([^"]*)"|([^'")\s]+))\s*\)`)
+
+func rewriteCSSRefs(body []byte, targetHost, ourHost string) []byte {
+ if targetHost == "" || ourHost == "" || strings.EqualFold(targetHost, ourHost) {
+ return body
+ }
+ return cssURLRe.ReplaceAllFunc(body, func(m []byte) []byte {
+ sm := cssURLRe.FindSubmatch(m)
+ var val []byte
+ if len(sm) > 1 && sm[1] != nil {
+ val = sm[1]
+ } else if len(sm) > 2 && sm[2] != nil {
+ val = sm[2]
+ } else if len(sm) > 3 && sm[3] != nil {
+ val = sm[3]
+ }
+ if val == nil {
+ return m
+ }
+ nv := []byte(rewriteURLRef(string(val), targetHost, ourHost))
+ return bytes.Replace(m, val, nv, 1)
+ })
+}
+
+func rewriteBodyRefs(body []byte, targetHost, ourHost string) []byte {
+ return rewriteCSSRefs(rewriteHTMLRefs(body, targetHost, ourHost), targetHost, ourHost)
+}
+
+type Listener struct {
+ server *http.Server
+ listener net.Listener
+ config *Config
+ paths []string
+ addConn internet.ConnHandler
+ proxy *httputil.ReverseProxy
+ target *url.URL
+}
+
+func ListenStealth(ctx context.Context, address xnet.Address, port xnet.Port, streamSettings *internet.MemoryStreamConfig, handler internet.ConnHandler) (internet.Listener, error) {
+ errors.LogInfo(ctx, "stealth listening on ", address, ":", port)
+ cfg, ok := streamSettings.ProtocolSettings.(*Config)
+ if !ok {
+ return nil, errors.New("stealth: invalid stream settings")
+ }
+
+ listener, err := internet.ListenSystem(ctx, &net.TCPAddr{IP: address.IP(), Port: int(port)}, streamSettings.SocketSettings)
+ if err != nil {
+ return nil, err
+ }
+
+ l := &Listener{
+ listener: listener,
+ config: cfg,
+ paths: cfg.GetNormalizedPaths(),
+ addConn: handler,
+ }
+ if target := cfg.GetNormalizedTarget(); target != nil {
+ // custom director: NewSingleHostReverseProxy in go>=1.26 does not
+ // rewrite req.Host, which makes real sites reject the request
+ rp := &httputil.ReverseProxy{
+ Director: func(req *http.Request) {
+ req.URL.Scheme = target.Scheme
+ req.URL.Host = target.Host
+ req.Host = target.Host
+ req.Header.Set("Accept-Encoding", "identity")
+ },
+ ModifyResponse: func(resp *http.Response) error {
+ host, _ := resp.Request.Context().Value(mirrorHostKey{}).(string)
+ if host == "" {
+ return nil
+ }
+ loc := resp.Header.Get("Location")
+ if loc != "" {
+ u, err := url.Parse(loc)
+ if err == nil && u.Host != "" {
+ // keep redirects inside our domain so the mirror feels seamless
+ if strings.EqualFold(u.Host, target.Host) {
+ u.Scheme = "https"
+ u.Host = host
+ resp.Header.Set("Location", u.String())
+ }
+ }
+ }
+ // rewrite absolute links in HTML/CSS so browsing stays on our domain
+ if isHTMLResponse(resp) || isCSSResponse(resp) {
+ if body, err := io.ReadAll(io.LimitReader(resp.Body, 8<<20)); err == nil && len(body) < 8<<20 {
+ var nb []byte
+ if isCSSResponse(resp) {
+ nb = rewriteCSSRefs(body, target.Host, host)
+ } else {
+ nb = rewriteBodyRefs(body, target.Host, host)
+ }
+ // always re-inject the body: io.ReadAll drained the
+ // original, so even an unchanged body must be re-wrapped
+ resp.Body.Close()
+ resp.Body = io.NopCloser(bytes.NewReader(nb))
+ resp.ContentLength = int64(len(nb))
+ resp.Header.Set("Content-Length", strconv.Itoa(len(nb)))
+ resp.Header.Del("Content-Encoding")
+ resp.Header.Del("Transfer-Encoding")
+ }
+ }
+ return nil
+ },
+ }
+ l.proxy = rp
+ l.target = target
+ errors.LogInfo(ctx, "stealth mirroring real site at ", target.String())
+ }
+
+ // wrap the raw listener with TLS or REALITY; the inner stream is served as
+ // plaintext HTTP/1.1 + h2c (same pattern as XHTTP)
+ if config := tls.ConfigFromStreamSettings(streamSettings); config != nil {
+ if tlsConfig := config.GetTLSConfig(); tlsConfig != nil {
+ tlsConfig.NextProtos = []string{"h2"}
+ if len(tlsConfig.Certificates) > 0 {
+ certs := tlsConfig.Certificates
+ tlsConfig.GetCertificate = func(*gotls.ClientHelloInfo) (*gotls.Certificate, error) {
+ return &certs[0], nil
+ }
+ }
+ listener = gotls.NewListener(listener, tlsConfig)
+ errors.LogInfo(ctx, "stealth TLS listener enabled")
+ }
+ }
+ if config := reality.ConfigFromStreamSettings(streamSettings); config != nil {
+ listener = goreality.NewListener(listener, config.GetREALITYConfig())
+ errors.LogInfo(ctx, "stealth REALITY listener enabled")
+ }
+
+ protocols := new(http.Protocols)
+ protocols.SetHTTP1(true)
+ protocols.SetUnencryptedHTTP2(true)
+ server := &http.Server{
+ Handler: l,
+ ReadHeaderTimeout: 10 * time.Second,
+ IdleTimeout: 0,
+ Protocols: protocols,
+ }
+ _ = http2.ConfigureServer(server, &http2.Server{})
+ l.server = server
+
+ go func() {
+ if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
+ errors.LogWarningInner(context.Background(), err, "stealth: http server stopped")
+ }
+ }()
+ return l, nil
+}
+
+func (l *Listener) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost || !containsString(l.paths, r.URL.Path) {
+ // probes and normal browsing see a real website (reverse-proxied),
+ // falling back to a built-in landing page when no target is set
+ if l.proxy != nil && (r.Method == http.MethodGet || r.Method == http.MethodHead) {
+ l.proxy.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), mirrorHostKey{}, r.Host)))
+ return
+ }
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ w.WriteHeader(http.StatusOK)
+ _, _ = io.WriteString(w, landingPage)
+ return
+ }
+
+ ftype, payload, err := readFrame(r.Body)
+ if err != nil || ftype != frameTypeAuth || string(payload) != l.config.Key {
+ http.NotFound(w, r)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/octet-stream")
+ w.WriteHeader(http.StatusOK)
+ var flush func()
+ if flusher, ok := w.(http.Flusher); ok {
+ flush = flusher.Flush
+ flusher.Flush()
+ }
+
+ var local, remote net.Addr
+ if v, ok := r.Context().Value(http.LocalAddrContextKey).(net.Addr); ok {
+ local = v
+ }
+ if raddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr); err == nil {
+ remote = raddr
+ }
+ if local == nil {
+ local = &net.TCPAddr{IP: net.IPv4zero}
+ }
+ if remote == nil {
+ remote = local
+ }
+ conn := newH2Conn(r.Body, w, nil, flush, l.config, local, remote)
+ l.addConn(stat.Connection(conn))
+
+ select {
+ case <-conn.Done():
+ case <-r.Context().Done():
+ }
+ _ = conn.Close()
+}
+
+func containsString(list []string, s string) bool {
+ for _, v := range list {
+ if v == s {
+ return true
+ }
+ }
+ return false
+}
+
+func (l *Listener) Addr() net.Addr {
+ return l.listener.Addr()
+}
+
+func (l *Listener) Close() error {
+ _ = l.server.Close()
+ return l.listener.Close()
+}
+
+func init() {
+ common.Must(internet.RegisterTransportListener(protocolName, ListenStealth))
+}