diff --git a/constant/proxy.go b/constant/proxy.go index f875d7e0..2d469d8f 100644 --- a/constant/proxy.go +++ b/constant/proxy.go @@ -21,6 +21,7 @@ const ( TypeShadowTLS = "shadowtls" TypeShadowsocksR = "shadowsocksr" TypeVLESS = "vless" + TypeMTProto = "mtproto" ) const ( diff --git a/docs/configuration/inbound/mtproto.md b/docs/configuration/inbound/mtproto.md new file mode 100644 index 00000000..bfe3d438 --- /dev/null +++ b/docs/configuration/inbound/mtproto.md @@ -0,0 +1,28 @@ +### Structure + +```json +{ + "type": "mtproto", + "tag": "mtproto-in", + + ... // Listen Fields + + "secret": "" +} +``` + +!!! warning "" + + MTProto is not included by default, see [Installation](/#installation). + +### Listen Fields + +See [Listen Fields](/configuration/shared/listen) for details. + +### Fields + +#### secret + +==Required== + +MTProto V3 secret. diff --git a/docs/configuration/inbound/mtproto.zh.md b/docs/configuration/inbound/mtproto.zh.md new file mode 100644 index 00000000..eab095a0 --- /dev/null +++ b/docs/configuration/inbound/mtproto.zh.md @@ -0,0 +1,28 @@ +### 结构 + +```json +{ + "type": "mtproto", + "tag": "mtproto-in", + + ... // 监听字段 + + "secret": "" +} +``` + +!!! warning "" + + 默认安装不包含 MTProto,参阅 [安装](/zh/#_2)。 + +### 监听字段 + +参阅 [监听字段](/zh/configuration/shared/listen/)。 + +### 字段 + +#### secret + +==必填== + +MTProto V3 密钥。 diff --git a/docs/index.md b/docs/index.md index abfd78e8..e080b38a 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,6 +30,7 @@ go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@lat | `with_shadowsocksr` | Build with ShadowsocksR support, see [ShadowsocksR outbound](./configuration/outbound/shadowsocksr). | | `with_ech` | Build with TLS ECH extension support for TLS outbound, see [TLS](./configuration/shared/tls#ech). | | `with_utls` | Build with [uTLS](https://github.com/refraction-networking/utls) support for TLS outbound, see [TLS](./configuration/shared/tls#utls). | +| `with_mtproto` | Build with MTProto inbound support, see [TLS](./configuration/inbound/mtproto). | | `with_acme` | Build with ACME TLS certificate issuer support, see [TLS](./configuration/shared/tls). | | `with_clash_api` | Build with Clash API support, see [Experimental](./configuration/experimental#clash-api-fields). | | `with_v2ray_api` | Build with V2Ray API support, see [Experimental](./configuration/experimental#v2ray-api-fields). | diff --git a/docs/index.zh.md b/docs/index.zh.md index 79f72ee5..7d199af9 100644 --- a/docs/index.zh.md +++ b/docs/index.zh.md @@ -30,6 +30,7 @@ go install -v -tags with_clash_api github.com/sagernet/sing-box/cmd/sing-box@lat | `with_shadowsocksr` | 启用 ShadowsocksR 支持,参阅 [ShadowsocksR 出站](./configuration/outbound/shadowsocksr)。 | | `with_ech` | 启用 TLS ECH 扩展支持,参阅 [TLS](./configuration/shared/tls#ech)。 | | `with_utls` | 启用 uTLS 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 | +| `with_mtproto` | 启用 MTProto 支持,参阅 [实验性](./configuration/inbound/mtproto)。 | | `with_acme` | 启用 ACME TLS 证书签发支持,参阅 [TLS](./configuration/shared/tls)。 | | `with_clash_api` | 启用 Clash API 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 | | `with_v2ray_api` | 启用 V2Rat API 支持,参阅 [实验性](./configuration/experimental#v2ray-api-fields)。 | diff --git a/inbound/builder.go b/inbound/builder.go index f9e4b18c..f617450d 100644 --- a/inbound/builder.go +++ b/inbound/builder.go @@ -41,6 +41,8 @@ func New(ctx context.Context, router adapter.Router, logger log.ContextLogger, o return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions) case C.TypeShadowTLS: return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions) + case C.TypeMTProto: + return NewMTProto(ctx, router, logger, options.Tag, options.MTProtoOptions) default: return nil, E.New("unknown inbound type: ", options.Type) } diff --git a/inbound/mtproto.go b/inbound/mtproto.go new file mode 100644 index 00000000..17ba20e0 --- /dev/null +++ b/inbound/mtproto.go @@ -0,0 +1,76 @@ +//go:build with_mtproto + +package inbound + +import ( + "context" + "net" + "os" + "time" + + "github.com/sagernet/sing-box/adapter" + C "github.com/sagernet/sing-box/constant" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + "github.com/sagernet/sing-box/transport/mtproto" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/replay" +) + +var ( + _ adapter.Inbound = (*MTProto)(nil) + _ adapter.InjectableInbound = (*MTProto)(nil) +) + +type MTProto struct { + myInboundAdapter + secret mtproto.Secret + replayCache replay.Filter +} + +func NewMTProto(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MTProtoInboundOptions) (*MTProto, error) { + inbound := &MTProto{ + myInboundAdapter: myInboundAdapter{ + protocol: C.TypeMTProto, + network: []string{N.NetworkTCP}, + ctx: ctx, + router: router, + logger: logger, + tag: tag, + listenOptions: options.ListenOptions, + }, + replayCache: replay.NewSimple(time.Minute), + } + inbound.connHandler = inbound + var err error + inbound.secret, err = mtproto.ParseSecret(options.Secret) + if err != nil { + return nil, err + } + return inbound, nil +} + +func (m *MTProto) NewConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext) error { + fakeTLSConn, err := mtproto.FakeTLSHandshake(ctx, conn, m.secret, m.replayCache) + if err != nil { + return err + } + dc, err := mtproto.Obfs2ClientHandshake(m.secret.Key[:], fakeTLSConn) + if err != nil { + return err + } + if !mtproto.AddressPool.IsValidDC(dc) { + return E.New("unknown DC: ", dc) + } + dcAddr := mtproto.AddressPool.GetV4(dc) + + metadata.Protocol = "mtproto" + metadata.Destination = dcAddr[0] + + return m.router.RouteConnection(ctx, fakeTLSConn, metadata) +} + +func (m *MTProto) NewPacketConnection(ctx context.Context, conn N.PacketConn, metadata adapter.InboundContext) error { + return os.ErrInvalid +} diff --git a/inbound/mtproto_stub.go b/inbound/mtproto_stub.go new file mode 100644 index 00000000..9ec3b8ba --- /dev/null +++ b/inbound/mtproto_stub.go @@ -0,0 +1,16 @@ +//go:build !with_mtproto + +package inbound + +import ( + "context" + + "github.com/sagernet/sing-box/adapter" + "github.com/sagernet/sing-box/log" + "github.com/sagernet/sing-box/option" + E "github.com/sagernet/sing/common/exceptions" +) + +func NewMTProto(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.MTProtoInboundOptions) (adapter.Inbound, error) { + return nil, E.New(`MTProto is not included in this build, rebuild with -tags with_mtproto`) +} diff --git a/mkdocs.yml b/mkdocs.yml index 1bf75758..935a2fd8 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -69,6 +69,7 @@ nav: - Naive: configuration/inbound/naive.md - Hysteria: configuration/inbound/hysteria.md - ShadowTLS: configuration/inbound/shadowtls.md + - MTProto: configuration/inbound/mtproto.md - Tun: configuration/inbound/tun.md - Redirect: configuration/inbound/redirect.md - TProxy: configuration/inbound/tproxy.md diff --git a/option/inbound.go b/option/inbound.go index e717c7ba..d54c32f5 100644 --- a/option/inbound.go +++ b/option/inbound.go @@ -22,6 +22,7 @@ type _Inbound struct { NaiveOptions NaiveInboundOptions `json:"-"` HysteriaOptions HysteriaInboundOptions `json:"-"` ShadowTLSOptions ShadowTLSInboundOptions `json:"-"` + MTProtoOptions MTProtoInboundOptions `json:"-"` } type Inbound _Inbound @@ -55,6 +56,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) { v = h.HysteriaOptions case C.TypeShadowTLS: v = h.ShadowTLSOptions + case C.TypeMTProto: + v = h.MTProtoOptions default: return nil, E.New("unknown inbound type: ", h.Type) } @@ -94,6 +97,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error { v = &h.HysteriaOptions case C.TypeShadowTLS: v = &h.ShadowTLSOptions + case C.TypeMTProto: + v = &h.MTProtoOptions default: return E.New("unknown inbound type: ", h.Type) } diff --git a/option/mtproto.go b/option/mtproto.go new file mode 100644 index 00000000..c5f656f0 --- /dev/null +++ b/option/mtproto.go @@ -0,0 +1,6 @@ +package option + +type MTProtoInboundOptions struct { + ListenOptions + Secret string `json:"secret"` +} diff --git a/transport/mtproto/conn.go b/transport/mtproto/conn.go new file mode 100644 index 00000000..67df2b14 --- /dev/null +++ b/transport/mtproto/conn.go @@ -0,0 +1,183 @@ +package mtproto + +import ( + "crypto/cipher" + "encoding/binary" + "net" + "sync" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + N "github.com/sagernet/sing/common/network" + "github.com/sagernet/sing/common/rw" +) + +var ( + _ net.Conn = (*FakeTLSConn)(nil) + _ N.ExtendedWriter = (*FakeTLSConn)(nil) +) + +type FakeTLSConn struct { + net.Conn + remain int + writeLock sync.Mutex + + clientEncryptor cipher.Stream + clientDecryptor cipher.Stream + + serverEncryptor cipher.Stream + serverDecryptor cipher.Stream + + unreadServerHandshake []byte + serverHandshakeMutex sync.Locker +} + +func (c *FakeTLSConn) SetupObfs2(en, de cipher.Stream) { + c.clientEncryptor = en + c.clientDecryptor = de + c.serverHandshakeMutex = &sync.Mutex{} +} + +func (c *FakeTLSConn) read(p []byte) (n int, err error) { + n, err = c.Conn.Read(p) + if c.clientDecryptor != nil { + c.clientDecryptor.XORKeyStream(p, p[:n]) + } + if c.serverEncryptor != nil { + c.serverEncryptor.XORKeyStream(p, p[:n]) + } + return +} + +func (c *FakeTLSConn) Write(p []byte) (n int, err error) { + lenP := len(p) + frame := buf.Get(5 + lenP) + frame[0] = TypeApplicationData + frame[1] = 0x03 + frame[2] = 0x03 + binary.BigEndian.PutUint16(frame[3:], uint16(len(p))) + if c.serverDecryptor != nil { + c.serverDecryptor.XORKeyStream(frame[5:], p) + } + if c.clientEncryptor != nil { + c.clientEncryptor.XORKeyStream(frame[5:], frame[5:5+lenP]) + } + c.writeLock.Lock() + _, err = c.Conn.Write(frame) + c.writeLock.Unlock() + buf.Put(frame) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *FakeTLSConn) WriteBuffer(buffer *buf.Buffer) error { + if c.serverDecryptor != nil { + c.serverDecryptor.XORKeyStream(buffer.Bytes(), buffer.Bytes()) + } + if c.clientEncryptor != nil { + c.clientEncryptor.XORKeyStream(buffer.Bytes(), buffer.Bytes()) + } + l := buffer.Len() + header := buffer.ExtendHeader(5) + header[0] = TypeApplicationData + header[1] = 0x03 + header[2] = 0x03 + binary.BigEndian.PutUint16(header[3:], uint16(l)) + return common.Error(c.Conn.Write(buffer.Bytes())) +} + +func (c *FakeTLSConn) Read(p []byte) (n int, err error) { + lenP := len(p) + if c.serverEncryptor == nil && c.serverHandshakeMutex != nil { + c.serverHandshakeMutex.Lock() + defer c.serverHandshakeMutex.Unlock() + if c.serverEncryptor == nil { + en, de, h := GenerateObfs2ServerHandshake() + lenH := len(h) + if lenH < lenP { + copy(p, h) + c.serverEncryptor = en + c.serverDecryptor = de + return lenH, nil + } else if lenH == lenP { + copy(p, h) + c.serverEncryptor = en + c.serverDecryptor = de + return lenH, nil + } else { // lenH > lenP + copy(p, h) + c.unreadServerHandshake = h[lenP:] + c.serverEncryptor = en + c.serverDecryptor = de + return lenP, nil + } + } + } + if lenH := len(c.unreadServerHandshake); lenH > 0 { + if lenH < lenP { + copy(p, c.unreadServerHandshake) + p = p[lenH:] + } else if lenH == lenP { + copy(p, c.unreadServerHandshake) + return lenH, nil + } else { // lenH > lenP + copy(p, c.unreadServerHandshake) + c.unreadServerHandshake = c.unreadServerHandshake[lenP:] + return lenP, nil + } + } + +read: + lenP = len(p) + if c.remain > 0 { + if c.remain >= lenP { + n, err = c.read(p) + c.remain -= n + return n, err + } + n, err = c.read(p[:c.remain]) + if err != nil { + return n, err + } + p = p[n:] //nolint:staticcheck + c.remain -= n + return n, nil + } + header := buf.Get(5) + defer buf.Put(header) + _, err = c.Conn.Read(header) + if err != nil { + return 0, err + } + l := int(binary.BigEndian.Uint16(header[3:])) + switch header[0] { + case TypeChangeCipherSpec: + err = rw.SkipN(c.Conn, l) + if err != nil { + return 0, err + } + goto read + case TypeApplicationData: + if lenP > l { + _n, err := c.read(p[:l]) + n += _n + return n, err + } else if lenP < l { + _n, err := c.read(p) + n += _n + c.remain = l - _n + return n, err + } + _n, err := c.read(p) + n += _n + return n, err + } + return n, E.New("unsupported record type: ", header[0]) +} + +func (c *FakeTLSConn) FrontHeadroom() int { + return 5 +} diff --git a/transport/mtproto/dc.go b/transport/mtproto/dc.go new file mode 100644 index 00000000..4cae6662 --- /dev/null +++ b/transport/mtproto/dc.go @@ -0,0 +1,115 @@ +package mtproto + +import ( + "math/rand" + + M "github.com/sagernet/sing/common/metadata" +) + +const ( + // DefaultDC defines a number of the default DC to use. This value used + // only if a value from obfuscated2 handshake frame is 0 (default). + DefaultDC = 2 +) + +// https://github.com/telegramdesktop/tdesktop/blob/master/Telegram/SourceFiles/mtproto/mtproto_dc_options.cpp#L30 +var ( + productionV4Addresses = [][]M.Socksaddr{ + { // dc1 + M.ParseSocksaddr("149.154.175.50:443"), + }, + { // dc2 + M.ParseSocksaddr("149.154.167.51:443"), + M.ParseSocksaddr("95.161.76.100:443"), + }, + { // dc3 + M.ParseSocksaddr("149.154.175.100:443"), + }, + { // dc4 + M.ParseSocksaddr("149.154.167.91:443"), + }, + { // dc5 + M.ParseSocksaddr("149.154.171.5:443"), + }, + } + productionV6Addresses = [][]M.Socksaddr{ + { // dc1 + M.ParseSocksaddr("[2001:b28:f23d:f001::a]:443"), + }, + { // dc2 + M.ParseSocksaddr("[2001:67c:04e8:f002::a]:443"), + }, + { // dc3 + M.ParseSocksaddr("[2001:b28:f23d:f003::a]:443"), + }, + { // dc4 + M.ParseSocksaddr("[2001:67c:04e8:f004::a]:443"), + }, + { // dc5 + M.ParseSocksaddr("[2001:b28:f23f:f005::a]:443"), + }, + } + + /*testV4Addresses = [][]M.Socksaddr{ + { // dc1 + M.ParseSocksaddr("149.154.175.10:443"), + }, + { // dc2 + M.ParseSocksaddr("149.154.167.40:443"), + }, + { // dc3 + M.ParseSocksaddr("149.154.175.117:443"), + }, + } + testV6Addresses = [][]M.Socksaddr{ + { // dc1 + M.ParseSocksaddr("[2001:b28:f23d:f001::e]:443"), + }, + { // dc2 + M.ParseSocksaddr("[2001:67c:04e8:f002::e]:443"), + }, + { // dc3 + M.ParseSocksaddr("[2001:b28:f23d:f003::e]:443"), + }, + }*/ +) + +type addressPool struct { + v4 [][]M.Socksaddr + v6 [][]M.Socksaddr +} + +var AddressPool = addressPool{productionV4Addresses, productionV6Addresses} + +func (a addressPool) IsValidDC(dc int) bool { + return dc > 0 && dc <= len(a.v4) && dc <= len(a.v6) +} + +func (a addressPool) getRandomDC() int { + return 1 + rand.Intn(len(a.v4)) +} + +func (a addressPool) GetV4(dc int) []M.Socksaddr { + return a.get(a.v4, dc-1) +} + +func (a addressPool) GetV6(dc int) []M.Socksaddr { + return a.get(a.v6, dc-1) +} + +func (a addressPool) get(addresses [][]M.Socksaddr, dc int) []M.Socksaddr { + if dc < 0 || dc >= len(addresses) { + return nil + } + + rv := make([]M.Socksaddr, len(addresses[dc])) + copy(rv, addresses[dc]) + + if len(rv) > 1 { + rand.Shuffle(len(rv), func(i, j int) { + rv[i], rv[j] = rv[j], rv[i] + }) + } + + return rv +} diff --git a/transport/mtproto/faketls.go b/transport/mtproto/faketls.go new file mode 100644 index 00000000..ab99da39 --- /dev/null +++ b/transport/mtproto/faketls.go @@ -0,0 +1,286 @@ +package mtproto + +import ( + "context" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + mrand "math/rand" + "net" + "time" + + "github.com/sagernet/sing/common" + "github.com/sagernet/sing/common/buf" + E "github.com/sagernet/sing/common/exceptions" + "github.com/sagernet/sing/common/replay" + + "golang.org/x/crypto/curve25519" +) + +const ( + // TypeChangeCipherSpec defines a byte value of the TLS record when a + // peer wants to change a specifications of the chosen cipher. + TypeChangeCipherSpec byte = 0x14 + + // TypeHandshake defines a byte value of the TLS record when a peer + // initiates a new TLS connection and wants to make a handshake + // ceremony. + TypeHandshake byte = 0x16 + + // TypeApplicationData defines a byte value of the TLS record when a + // peer sends an user data, not a control frames. + TypeApplicationData byte = 0x17 + + // Version10 defines a TLS1.0. + Version10 uint16 = 769 // 0x03 0x01 + + // Version11 defines a TLS1.1. + Version11 uint16 = 770 // 0x03 0x02 + + // Version12 defines a TLS1.2. + Version12 uint16 = 771 // 0x03 0x03 + + // Version13 defines a TLS1.3. + Version13 uint16 = 772 // 0x03 0x04 +) + +var ( + emptyRandom = make([]byte, 32) + serverHelloSuffix = []byte{ + 0x00, // no compression + 0x00, 0x2e, // 46 bytes of data + 0x00, 0x2b, // Extension - Supported Versions + 0x00, 0x02, // 2 bytes are following + 0x03, 0x04, // TLS 1.3 + 0x00, 0x33, // Extension - Key Share + 0x00, 0x24, // 36 bytes + 0x00, 0x1d, // x25519 curve + 0x00, 0x20, // 32 bytes of key + } + serverChangeCipherSpec = []byte{ + 0x14, // record type ChangeCipherSpec + 0x03, 0x03, // v1.2 + 0x00, 0x01, // payload length (1, big endian) + 0x01, // payload - magic + } +) + +func FakeTLSHandshake(ctx context.Context, conn net.Conn, secret Secret, replay replay.Filter) (*FakeTLSConn, error) { + _record := buf.StackNew() + defer common.KeepAlive(_record) + record := common.Dup(_record) + defer record.Release() + + /*var recordHeaderLen int + recordHeaderLen+=1 // type + recordHeaderLen+=2 // version + recordHeaderLen+=2 // payload length*/ + _, err := record.ReadFullFrom(conn, 5) + if err != nil { + return nil, E.Cause(err, "read FakeTLS record") + } + + recordType := record.Byte(0) + switch recordType { + case TypeChangeCipherSpec, TypeHandshake, TypeApplicationData: + default: + return nil, E.New("unknown record type: ", recordType) + } + + version := binary.BigEndian.Uint16(record.Range(1, 3)) + switch version { + case Version10, Version11, Version12, Version13: + default: + return nil, E.New("unknown tls version: ", version) + } + + length := int(binary.BigEndian.Uint16(record.Range(3, 5))) + record.Reset() + _, err = record.ReadFullFrom(conn, length) + if err != nil { + return nil, E.Cause(err, "read FakeTLS record") + } + + hello, err := parseClientHello(secret, record) + if err != nil { + return nil, err + } + + err = hello.Valid(secret.Host, time.Minute) + if err != nil { + return nil, err + } + + if !replay.Check(hello.SessionID) { + return nil, E.New("replay attack detected: ", hex.EncodeToString(hello.SessionID)) + } + + _serverHello := buf.StackNew() + defer common.KeepAlive(_serverHello) + serverHello := common.Dup(_serverHello) + defer serverHello.Release() + + generateServerHello(serverHello, hello) + common.Must1(serverHello.Write(serverChangeCipherSpec)) + + mac := hmac.New(sha256.New, secret.Key[:]) + mac.Write(hello.Random[:]) + + appDataHeader := serverHello.Extend(5) + appDataRandomLen := 1024 + mrand.Intn(3092) + appDataHeader[0] = TypeApplicationData + appDataHeader[1] = 0x03 // v1.2 + appDataHeader[2] = 0x03 // v1.2 + binary.BigEndian.PutUint16(appDataHeader[3:], uint16(appDataRandomLen)) + serverHello.WriteRandom(appDataRandomLen) + + mac.Write(serverHello.Bytes()) + copy(serverHello.From(11), mac.Sum(nil)) + + _, err = serverHello.WriteTo(conn) + if err != nil { + return nil, err + } + return &FakeTLSConn{Conn: conn}, nil +} + +type ClientHello struct { + Time time.Time + Random [32]byte + SessionID []byte + Host string + CipherSuite uint16 +} + +func (c *ClientHello) Valid(hostname string, tolerateTimeSkewness time.Duration) error { + if c.Host != "" && c.Host != hostname { + return E.New("incorrect hostname: ", hostname) + } + + now := time.Now() + + timeDiff := now.Sub(c.Time) + if timeDiff < 0 { + timeDiff = -timeDiff + } + + if timeDiff > tolerateTimeSkewness { + return E.New("incorrect timestamp. got=", + c.Time.Unix(), ",now= ", now.Unix(), ", diff=", timeDiff.String()) + } + + return nil +} + +func parseClientHello(secret Secret, handshake *buf.Buffer) (*ClientHello, error) { + l := handshake.Len() + if l < 6 { // minimum client hello length + return nil, E.New("client hello too short: ", l) + } + if t := handshake.Byte(0); t != 0x01 { // handshake type client + return nil, E.New("unknown handshake type: ", t) + } + + handshakeLen := int(binary.BigEndian.Uint32([]byte{0, handshake.Byte(1), handshake.Byte(2), handshake.Byte(3)})) + if l-4 != handshakeLen { + return nil, E.New("incorrect handshake size. manifested=", handshakeLen, ", got=", l-4) + } + + hello := &ClientHello{} + + mac := hmac.New(sha256.New, secret.Key[:]) + mac.Write([]byte{TypeHandshake, 0x03, 0x01}) + var payloadLen [2]byte + binary.BigEndian.PutUint16(payloadLen[:], uint16(l)) + mac.Write(payloadLen[:]) + mac.Write(handshake.Range(0, 6)) + mac.Write(emptyRandom) + mac.Write(handshake.From(38)) + computedRandom := mac.Sum(nil) + for i := 0; i < 32; i++ { + computedRandom[i] ^= handshake.Byte(6 + i) + } + if subtle.ConstantTimeCompare(emptyRandom[:32-4], computedRandom[:32-4]) != 1 { + return nil, E.New("bad digest") + } + + timestamp := int64(binary.LittleEndian.Uint32(computedRandom[32-4:])) + hello.Time = time.Unix(timestamp, 0) + + copy(hello.Random[:], handshake.Range(6, 38)) + + parseSessionID(hello, handshake) + parseCipherSuite(hello, handshake) + parseSNI(hello, handshake.Bytes()) + + return hello, nil +} + +func parseSessionID(hello *ClientHello, handshake *buf.Buffer) { + hello.SessionID = make([]byte, handshake.Byte(38)) + copy(hello.SessionID, handshake.From(38+1)) +} + +func parseCipherSuite(hello *ClientHello, handshake *buf.Buffer) { + cipherSuiteOffset := 38 + len(hello.SessionID) + 3 //nolint: gomnd + hello.CipherSuite = binary.BigEndian.Uint16(handshake.Range(cipherSuiteOffset, cipherSuiteOffset+2)) +} + +func parseSNI(hello *ClientHello, handshake []byte) { + cipherSuiteOffset := 38 + len(hello.SessionID) + 1 + handshake = handshake[cipherSuiteOffset:] + + cipherSuiteLength := binary.BigEndian.Uint16(handshake[:2]) + handshake = handshake[2+cipherSuiteLength:] + + compressionMethodsLength := int(handshake[0]) + handshake = handshake[1+compressionMethodsLength:] + + extensionsLength := binary.BigEndian.Uint16(handshake[:2]) + handshake = handshake[2 : 2+extensionsLength] + + for len(handshake) > 0 { + if binary.BigEndian.Uint16(handshake[:2]) != 0x00 { // extension SNI + extensionsLength := binary.BigEndian.Uint16(handshake[2:4]) + handshake = handshake[4+extensionsLength:] + + continue + } + + hostnameLength := binary.BigEndian.Uint16(handshake[7:9]) + handshake = handshake[9:] + hello.Host = string(handshake[:int(hostnameLength)]) + + return + } +} + +func generateServerHello(record *buf.Buffer, ch *ClientHello) { + common.Must1(record.Write([]byte{0x03, 0x03})) // v1.2 + common.Must(record.WriteZeroN(32)) + common.Must(record.WriteByte(byte(len(ch.SessionID)))) + common.Must1(record.Write(ch.SessionID)) + binary.BigEndian.PutUint16(record.Extend(2), ch.CipherSuite) + common.Must1(record.Write(serverHelloSuffix)) + + scalar := buf.Get(32) + defer buf.Put(scalar) + common.Must1(rand.Read(scalar)) + curve, _ := curve25519.X25519(scalar, curve25519.Basepoint) + common.Must1(record.Write(curve)) + + l := record.Len() + header := record.ExtendHeader(4) + binary.BigEndian.PutUint32(header, uint32(l)) + header[0] = 0x02 // handshake type server + + l = record.Len() + header = record.ExtendHeader(5) + header[0] = TypeHandshake + header[1] = 0x03 // v1.2 + header[2] = 0x03 // v1.2 + binary.BigEndian.PutUint16(header[3:], uint16(l)) +} diff --git a/transport/mtproto/obfs2.go b/transport/mtproto/obfs2.go new file mode 100644 index 00000000..55944e02 --- /dev/null +++ b/transport/mtproto/obfs2.go @@ -0,0 +1,187 @@ +package mtproto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha256" + "crypto/subtle" + "encoding/binary" + "encoding/hex" + "io" + + E "github.com/sagernet/sing/common/exceptions" +) + +const ( + handshakeFrameLen = 64 + + handshakeFrameLenKey = 32 + handshakeFrameLenIV = 16 + handshakeFrameLenConnectionType = 4 + + handshakeFrameOffsetStart = 8 + handshakeFrameOffsetKey = handshakeFrameOffsetStart + handshakeFrameOffsetIV = handshakeFrameOffsetKey + handshakeFrameLenKey + handshakeFrameOffsetConnectionType = handshakeFrameOffsetIV + handshakeFrameLenIV + handshakeFrameOffsetDC = handshakeFrameOffsetConnectionType + handshakeFrameLenConnectionType +) + +// Connection-Type: Secure. We support only fake tls. +var handshakeConnectionType = []byte{0xdd, 0xdd, 0xdd, 0xdd} + +// A structure of obfuscated2 handshake frame is following: +// +// [frameOffsetFirst:frameOffsetKey:frameOffsetIV:frameOffsetMagic:frameOffsetDC:frameOffsetEnd]. +// +// - 8 bytes of noise +// - 32 bytes of AES Key +// - 16 bytes of AES IV +// - 4 bytes of 'connection type' - this has some setting like a connection type +// - 2 bytes of 'DC'. DC is little endian int16 +// - 2 bytes of noise +type handshakeFrame struct { + data [handshakeFrameLen]byte +} + +type clientHandshakeFrame struct { + handshakeFrame +} + +func (f *clientHandshakeFrame) dc() int { + idx := int16(f.data[handshakeFrameOffsetDC]) | int16(f.data[handshakeFrameOffsetDC+1])<<8 //nolint: gomnd, lll // little endian for int16 is here + + switch { + case idx > 0: + return int(idx) + case idx < 0: + return -int(idx) + default: + return DefaultDC + } +} + +func (f *handshakeFrame) key() []byte { + return f.data[handshakeFrameOffsetKey:handshakeFrameOffsetIV] +} + +func (f *handshakeFrame) iv() []byte { + return f.data[handshakeFrameOffsetIV:handshakeFrameOffsetConnectionType] +} + +func (f *handshakeFrame) connectionType() []byte { + return f.data[handshakeFrameOffsetConnectionType:handshakeFrameOffsetDC] +} + +func (f *handshakeFrame) invert() *clientHandshakeFrame { + copyFrame := &clientHandshakeFrame{} + + for i := 0; i < handshakeFrameLenKey+handshakeFrameLenIV; i++ { + copyFrame.data[handshakeFrameOffsetKey+i] = f.data[handshakeFrameOffsetConnectionType-1-i] + } + + return copyFrame +} + +func (f *clientHandshakeFrame) decryptor(secret []byte) cipher.Stream { + hasher := sha256.New() + + hasher.Write(f.key()) + hasher.Write(secret) + + return makeAesCtr(hasher.Sum(nil), f.iv()) +} + +func (f *clientHandshakeFrame) encryptor(secret []byte) cipher.Stream { + invertedHandshake := f.invert() + + hasher := sha256.New() + + hasher.Write(invertedHandshake.key()) + hasher.Write(secret) + + return makeAesCtr(hasher.Sum(nil), invertedHandshake.iv()) +} + +func makeAesCtr(key, iv []byte) cipher.Stream { + block, err := aes.NewCipher(key) + if err != nil { + panic(err) + } + + return cipher.NewCTR(block, iv) +} + +type serverHandshakeFrame struct { + handshakeFrame +} + +func (s *serverHandshakeFrame) decryptor() cipher.Stream { + invertedHandshake := s.invert() + + return makeAesCtr(invertedHandshake.key(), invertedHandshake.iv()) +} + +func (s *serverHandshakeFrame) encryptor() cipher.Stream { + return makeAesCtr(s.key(), s.iv()) +} + +func GenerateObfs2ServerHandshake() (cipher.Stream, cipher.Stream, []byte) { + handshake := generateServerHanshakeFrame() + copyHandshake := handshake + encryptor := handshake.encryptor() + decryptor := handshake.decryptor() + + encryptor.XORKeyStream(handshake.data[:], handshake.data[:]) + copy(handshake.key(), copyHandshake.key()) + copy(handshake.iv(), copyHandshake.iv()) + + return encryptor, decryptor, handshake.data[:] +} + +func generateServerHanshakeFrame() serverHandshakeFrame { + frame := serverHandshakeFrame{} + + for { + if _, err := rand.Read(frame.data[:]); err != nil { + panic(err) + } + + if frame.data[0] == 0xEF { //nolint: gomnd // taken from tg sources + continue + } + + switch binary.LittleEndian.Uint32(frame.data[:4]) { + case 0x44414548, 0x54534F50, 0x20544547, 0x4954504F, 0xEEEEEEEE: //nolint: gomnd // taken from tg sources + continue + } + + if frame.data[4]|frame.data[5]|frame.data[6]|frame.data[7] == 0 { + continue + } + + copy(frame.connectionType(), handshakeConnectionType) + + return frame + } +} + +func Obfs2ClientHandshake(secret []byte, conn *FakeTLSConn) (int, error) { + handshake := &clientHandshakeFrame{} + + if _, err := io.ReadFull(conn, handshake.data[:]); err != nil { + return 0, E.Cause(err, "cannot read frame") + } + + decryptor := handshake.decryptor(secret) + encryptor := handshake.encryptor(secret) + + decryptor.XORKeyStream(handshake.data[:], handshake.data[:]) + + if val := handshake.connectionType(); subtle.ConstantTimeCompare(handshakeConnectionType, val) != 1 { + return 0, E.New("unsupported connection type: ", hex.EncodeToString(val)) + } + + conn.SetupObfs2(encryptor, decryptor) + return handshake.dc(), nil +} diff --git a/transport/mtproto/secret.go b/transport/mtproto/secret.go new file mode 100644 index 00000000..dc7b44b2 --- /dev/null +++ b/transport/mtproto/secret.go @@ -0,0 +1,138 @@ +package mtproto + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + + "github.com/sagernet/sing/common" + E "github.com/sagernet/sing/common/exceptions" +) + +// kanged from https://github.com/9seconds/mtg/blob/master/mtglib/secret.go + +const ( + secretFakeTLSFirstByte byte = 0xEE + + SecretKeyLength = 16 +) + +var ( + secretEmptyKey [SecretKeyLength]byte + + ErrSecretEmpty = E.New("mtproto: secret is empty") +) + +// Secret is a data structure that presents a secret. +// +// Telegram secret is not a simple string like +// "ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d". +// Actually, this is a serialized datastructure of 2 parts: key and host. +// +// ee367a189aee18fa31c190054efd4a8e9573746f726167652e676f6f676c65617069732e636f6d +// |-|-------------------------------|------------------------------------------- +// p key hostname +// +// Serialized secret starts with 'ee'. Actually, in the past we also had 'dd' +// secrets and prefixless ones. But this is history. Currently, we do have only +// 'ee' secrets which mean faketls + protection from statistical attacks on a +// length. 'ee' is a byte 238 (0xee). +// +// After that, we have 16 bytes of the key. This is a random generated secret +// data of the proxy and this data is used to derive authentication schemas. +// These secrets are mixed into hmacs and sha256 checksums which are used to +// build AEAD ciphers for obfuscated2 protocol and ensure faketls handshake. +// +// Host is a domain fronting hostname in latin1 (ASCII) encoding. This hostname +// should be used for SNI in faketls and sing-box verifies it. Also, this is when +// sing-box gets about a domain fronting hostname. +// +// Secrets can be serialized into 2 forms: hex and base64. If you decode both +// forms into bytes, you'll get the same byte array. Telegram clients nowadays +// accept all forms. +type Secret struct { + // Key is a set of bytes used for traffic authentication. + Key [SecretKeyLength]byte + + // Host is a domain fronting hostname. + Host string +} + +func (s *Secret) Set(text string) error { + if text == "" { + return ErrSecretEmpty + } + + decoded, err := hex.DecodeString(text) + if err != nil { + decoded, err = base64.RawURLEncoding.DecodeString(text) + } + + if err != nil { + return E.New("incorrect secret format: ", err) + } + + l := len(decoded) + if l < 2 { //nolint: gomnd // we need at least 1 byte here + return E.New("secret is truncated, length=", l) + } + + if decoded[0] != secretFakeTLSFirstByte { + return E.New("incorrect first byte of secret: ", decoded[0]) + } + + if l < 1+SecretKeyLength { // 1 for FakeTLS first byte + return E.New("secret has incorrect length ", len(decoded)) + } + + copy(s.Key[:], decoded[1:SecretKeyLength+1]) + s.Host = string(decoded[1+SecretKeyLength:]) + + if s.Host == "" { + return E.New("hostname cannot be empty: ", text) + } + + return nil +} + +// Valid checks if this secret is valid and can be used in proxy. +func (s Secret) Valid() bool { + return s.Key != secretEmptyKey && s.Host != "" +} + +// String is to support fmt.Stringer interface. +func (s Secret) String() string { + return s.Base64() +} + +// Base64 returns a base64-encoded form of this secret. +func (s Secret) Base64() string { + return base64.RawURLEncoding.EncodeToString(s.makeBytes()) +} + +// Hex returns a hex-encoded form of this secret (ee-secret). +func (s Secret) Hex() string { + return hex.EncodeToString(s.makeBytes()) +} + +func (s *Secret) makeBytes() []byte { + data := append([]byte{secretFakeTLSFirstByte}, s.Key[:]...) + data = append(data, s.Host...) + + return data +} + +// GenerateSecret makes a new secret with a given hostname. +func GenerateSecret(hostname string) Secret { + s := Secret{Host: hostname} + common.Must1(rand.Read(s.Key[:])) + + return s +} + +// ParseSecret parses a secret (both hex and base64 forms). +func ParseSecret(secret string) (Secret, error) { + s := Secret{} + + return s, s.Set(secret) +}