mirror of
https://github.com/SagerNet/sing-box.git
synced 2025-06-13 21:54:13 +08:00
Add MTProto inbound
This commit is contained in:
parent
f32c149738
commit
b458d3cd03
@ -21,6 +21,7 @@ const (
|
|||||||
TypeShadowTLS = "shadowtls"
|
TypeShadowTLS = "shadowtls"
|
||||||
TypeShadowsocksR = "shadowsocksr"
|
TypeShadowsocksR = "shadowsocksr"
|
||||||
TypeVLESS = "vless"
|
TypeVLESS = "vless"
|
||||||
|
TypeMTProto = "mtproto"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
28
docs/configuration/inbound/mtproto.md
Normal file
28
docs/configuration/inbound/mtproto.md
Normal file
@ -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.
|
28
docs/configuration/inbound/mtproto.zh.md
Normal file
28
docs/configuration/inbound/mtproto.zh.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
### 结构
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "mtproto",
|
||||||
|
"tag": "mtproto-in",
|
||||||
|
|
||||||
|
... // 监听字段
|
||||||
|
|
||||||
|
"secret": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! warning ""
|
||||||
|
|
||||||
|
默认安装不包含 MTProto,参阅 [安装](/zh/#_2)。
|
||||||
|
|
||||||
|
### 监听字段
|
||||||
|
|
||||||
|
参阅 [监听字段](/zh/configuration/shared/listen/)。
|
||||||
|
|
||||||
|
### 字段
|
||||||
|
|
||||||
|
#### secret
|
||||||
|
|
||||||
|
==必填==
|
||||||
|
|
||||||
|
MTProto V3 密钥。
|
@ -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_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_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_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_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_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). |
|
| `with_v2ray_api` | Build with V2Ray API support, see [Experimental](./configuration/experimental#v2ray-api-fields). |
|
||||||
|
@ -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_shadowsocksr` | 启用 ShadowsocksR 支持,参阅 [ShadowsocksR 出站](./configuration/outbound/shadowsocksr)。 |
|
||||||
| `with_ech` | 启用 TLS ECH 扩展支持,参阅 [TLS](./configuration/shared/tls#ech)。 |
|
| `with_ech` | 启用 TLS ECH 扩展支持,参阅 [TLS](./configuration/shared/tls#ech)。 |
|
||||||
| `with_utls` | 启用 uTLS 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
|
| `with_utls` | 启用 uTLS 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
|
||||||
|
| `with_mtproto` | 启用 MTProto 支持,参阅 [实验性](./configuration/inbound/mtproto)。 |
|
||||||
| `with_acme` | 启用 ACME TLS 证书签发支持,参阅 [TLS](./configuration/shared/tls)。 |
|
| `with_acme` | 启用 ACME TLS 证书签发支持,参阅 [TLS](./configuration/shared/tls)。 |
|
||||||
| `with_clash_api` | 启用 Clash API 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
|
| `with_clash_api` | 启用 Clash API 支持,参阅 [实验性](./configuration/experimental#clash-api-fields)。 |
|
||||||
| `with_v2ray_api` | 启用 V2Rat API 支持,参阅 [实验性](./configuration/experimental#v2ray-api-fields)。 |
|
| `with_v2ray_api` | 启用 V2Rat API 支持,参阅 [实验性](./configuration/experimental#v2ray-api-fields)。 |
|
||||||
|
@ -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)
|
return NewHysteria(ctx, router, logger, options.Tag, options.HysteriaOptions)
|
||||||
case C.TypeShadowTLS:
|
case C.TypeShadowTLS:
|
||||||
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
|
return NewShadowTLS(ctx, router, logger, options.Tag, options.ShadowTLSOptions)
|
||||||
|
case C.TypeMTProto:
|
||||||
|
return NewMTProto(ctx, router, logger, options.Tag, options.MTProtoOptions)
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", options.Type)
|
return nil, E.New("unknown inbound type: ", options.Type)
|
||||||
}
|
}
|
||||||
|
76
inbound/mtproto.go
Normal file
76
inbound/mtproto.go
Normal file
@ -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
|
||||||
|
}
|
16
inbound/mtproto_stub.go
Normal file
16
inbound/mtproto_stub.go
Normal file
@ -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`)
|
||||||
|
}
|
@ -69,6 +69,7 @@ nav:
|
|||||||
- Naive: configuration/inbound/naive.md
|
- Naive: configuration/inbound/naive.md
|
||||||
- Hysteria: configuration/inbound/hysteria.md
|
- Hysteria: configuration/inbound/hysteria.md
|
||||||
- ShadowTLS: configuration/inbound/shadowtls.md
|
- ShadowTLS: configuration/inbound/shadowtls.md
|
||||||
|
- MTProto: configuration/inbound/mtproto.md
|
||||||
- Tun: configuration/inbound/tun.md
|
- Tun: configuration/inbound/tun.md
|
||||||
- Redirect: configuration/inbound/redirect.md
|
- Redirect: configuration/inbound/redirect.md
|
||||||
- TProxy: configuration/inbound/tproxy.md
|
- TProxy: configuration/inbound/tproxy.md
|
||||||
|
@ -22,6 +22,7 @@ type _Inbound struct {
|
|||||||
NaiveOptions NaiveInboundOptions `json:"-"`
|
NaiveOptions NaiveInboundOptions `json:"-"`
|
||||||
HysteriaOptions HysteriaInboundOptions `json:"-"`
|
HysteriaOptions HysteriaInboundOptions `json:"-"`
|
||||||
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
|
ShadowTLSOptions ShadowTLSInboundOptions `json:"-"`
|
||||||
|
MTProtoOptions MTProtoInboundOptions `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Inbound _Inbound
|
type Inbound _Inbound
|
||||||
@ -55,6 +56,8 @@ func (h Inbound) MarshalJSON() ([]byte, error) {
|
|||||||
v = h.HysteriaOptions
|
v = h.HysteriaOptions
|
||||||
case C.TypeShadowTLS:
|
case C.TypeShadowTLS:
|
||||||
v = h.ShadowTLSOptions
|
v = h.ShadowTLSOptions
|
||||||
|
case C.TypeMTProto:
|
||||||
|
v = h.MTProtoOptions
|
||||||
default:
|
default:
|
||||||
return nil, E.New("unknown inbound type: ", h.Type)
|
return nil, E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
@ -94,6 +97,8 @@ func (h *Inbound) UnmarshalJSON(bytes []byte) error {
|
|||||||
v = &h.HysteriaOptions
|
v = &h.HysteriaOptions
|
||||||
case C.TypeShadowTLS:
|
case C.TypeShadowTLS:
|
||||||
v = &h.ShadowTLSOptions
|
v = &h.ShadowTLSOptions
|
||||||
|
case C.TypeMTProto:
|
||||||
|
v = &h.MTProtoOptions
|
||||||
default:
|
default:
|
||||||
return E.New("unknown inbound type: ", h.Type)
|
return E.New("unknown inbound type: ", h.Type)
|
||||||
}
|
}
|
||||||
|
6
option/mtproto.go
Normal file
6
option/mtproto.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package option
|
||||||
|
|
||||||
|
type MTProtoInboundOptions struct {
|
||||||
|
ListenOptions
|
||||||
|
Secret string `json:"secret"`
|
||||||
|
}
|
183
transport/mtproto/conn.go
Normal file
183
transport/mtproto/conn.go
Normal file
@ -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
|
||||||
|
}
|
115
transport/mtproto/dc.go
Normal file
115
transport/mtproto/dc.go
Normal file
@ -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
|
||||||
|
}
|
286
transport/mtproto/faketls.go
Normal file
286
transport/mtproto/faketls.go
Normal file
@ -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))
|
||||||
|
}
|
187
transport/mtproto/obfs2.go
Normal file
187
transport/mtproto/obfs2.go
Normal file
@ -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
|
||||||
|
}
|
138
transport/mtproto/secret.go
Normal file
138
transport/mtproto/secret.go
Normal file
@ -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)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user