MatsuriDayo / NekoBoxForAndroid

NekoBox for Android / sing-box / universal proxy toolchain for Android
https://matsuridayo.github.io/
Other
11.8k stars 990 forks source link

请给出 sn:// 生成规范文档 #298

Closed zz5678 closed 1 year ago

zz5678 commented 1 year ago

这个 sn://wg? 其后的编码方式到底是怎样的? 用NekoBox导出的 sn link 去掉头部 base64 无论那种都根本无法解码,无法逆向还原,通过下面提示源码只能看到去掉空格[ ],编码 base64而已,原始数据格式到底是什么?

比如 wireguard 的原始数据是这样的吗? ''' {name:CFWarp-A,type:wireguard,server:123.123.123.123,port:8854,ip:172.16.0.2,private-key:STH=,public-key:STH=,remote-dns-resolve:false,udp:true} '''

请给出原始规范文档

https://github.com/EsotericSoftware/kryo

https://github.com/MatsuriDayo/NekoBoxForAndroid/blob/d1da522700ecaa4650bf3b522f63992b2f426a6d/app/src/main/java/io/nekohasekai/sagernet/fmt/wireguard/WireGuardFmt.kt

Originally posted by @arm64v8a in https://github.com/MatsuriDayo/NekoBoxForAndroid/issues/270#issuecomment-1669132449

arm64v8a commented 1 year ago

这个格式需要问「世界」

zz5678 commented 1 year ago

这个格式需要问「世界」

就算暗网的世界也应该有个 url 吧?

arm64v8a commented 1 year ago

这个格式是沿用世界 SagerNet 的,具体实现在 kyro 库中。我无法提供其中的实现细节。

plusls commented 10 months ago

我看了一眼代码,随手糊了一个python的生成:

def encode_sn_str(s: str) -> bytes:
    if not s:
        return b'\x81'
    if len(s) == 1:
        return b'\x82' + s.encode()
    ret = s.encode()
    ret = ret[:-1] + (ret[-1] | 0x80).to_bytes(1, 'little')
    return ret

def p32(n: int):
    return n.to_bytes(4, 'little')

def p8(n: int):
    return n.to_bytes(1, 'little')

# https://github.com/MatsuriDayo/NekoBoxForAndroid/blob/main/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java

@dataclass(init=True, repr=True)
class SnBase:
    def serlize(self) -> bytes:
        ret = b''
        for _, v in self.__dict__.items():
            ret += self.obj_serlize(v)
        return ret

    @classmethod
    def obj_serlize(cls, obj) -> bytes:
        ret = b''
        match type(obj):
            case __builtins__.str:
                ret += encode_sn_str(obj)
            case __builtins__.bool:
                ret += p8(int(obj))
            case __builtins__.int:
                ret += p32(obj)
            case _:
                if (isinstance(obj, SnBase)):
                    ret += obj.serlize()
        return ret

@dataclass(init=True, repr=True)
class SnServer(SnBase):
    server_address: str = ''
    server_port: int = 0

@dataclass(init=True, repr=True)
class SnMeta(SnBase):
    # abstract bean
    extra_version: int = 1
    name: str = ''
    custom_outbound: str = ''
    custom_config: str = ''

@dataclass(init=True, repr=True)
class Tls(SnBase):
    sni: str = ''
    alpn: str = ''
    certificates: str = ''
    allow_insecure: bool = False
    utls_fingerprint: str = ''
    reality_pubkey: str = ''
    reality_short_id: str = ''

@dataclass(init=True, repr=True)
class Websocket(SnBase):
    host: str = ''
    path: str = ''
    max_early_data: int = 0
    early_data_header_name: str = ''

@dataclass(init=True, repr=True)
class V2ray(SnBase):
    version: int = 0
    server: SnServer = field(default_factory=SnServer)
    uuid: str = ''
    encryption: str = ''
    transport_type: str = ''
    transport: Any = None
    tls: Optional[Tls] = None
    # 1 packet 2: xudp
    packet_encoding: int = 0

    def serlize(self) -> bytes:
        ret = b''
        ret += self.obj_serlize(self.version)
        ret += self.obj_serlize(self.server)
        ret += self.obj_serlize(self.uuid)
        ret += self.obj_serlize(self.encryption)
        ret += self.obj_serlize(self.transport_type)
        match self.transport_type:
            case 'ws':
                assert (isinstance(self.transport, Websocket))
                ret += self.transport.serlize()
        ret += self.obj_serlize('tls' if self.tls else 'none')
        if self.tls:
            ret += self.tls.serlize()
        ret += self.obj_serlize(self.packet_encoding)
        return ret

@dataclass(init=True, repr=True)
class Trojan(SnBase):
    version: int = 2
    v2ray: V2ray = field(default_factory=V2ray)
    password: str = ''
    sn_meta: SnMeta = field(default_factory=SnMeta)

    def __str__(self) -> str:
        print(self.serlize())
        return f'sn://trojan?{base64.urlsafe_b64encode(zlib.compress(self.serlize())).decode()}'

v2ray = V2ray(server = SnServer('1.1.1.1', 443), 
              transport_type = 'ws', 
              transport = Websocket('abc', '/plusls', 2048, 'Sec-WebSocket-Protocol'), 
              tls = Tls('aa', 'bb', 'cc', True, 'chrome', 'd'))

trojan = Trojan(v2ray=v2ray, password='plusls', sn_meta = SnMeta(name='fuck', custom_outbound='{"a": 1}'))
print(trojan)