mirror of
https://github.com/SagerNet/sing-box.git
synced 2024-11-25 10:01:30 +00:00
platform: Add support for tvOS
This commit is contained in:
parent
1e31d26e03
commit
b054441f34
4
Makefile
4
Makefile
|
@ -89,8 +89,8 @@ lib:
|
||||||
|
|
||||||
lib_install:
|
lib_install:
|
||||||
go get -v -d
|
go get -v -d
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230701084532-493ee2e45182
|
go install -v github.com/sagernet/gomobile/cmd/gomobile@v0.0.0-20230728014906-3de089147f59
|
||||||
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230701084532-493ee2e45182
|
go install -v github.com/sagernet/gomobile/cmd/gobind@v0.0.0-20230728014906-3de089147f59
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf bin dist sing-box
|
rm -rf bin dist sing-box
|
||||||
|
|
|
@ -107,7 +107,7 @@ func buildiOS() {
|
||||||
args := []string{
|
args := []string{
|
||||||
"bind",
|
"bind",
|
||||||
"-v",
|
"-v",
|
||||||
"-target", "ios,iossimulator,macos",
|
"-target", "ios,iossimulator,tvos,tvossimulator,macos",
|
||||||
"-libname=box",
|
"-libname=box",
|
||||||
}
|
}
|
||||||
if !debugEnabled {
|
if !debugEnabled {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandClient struct {
|
type CommandClient struct {
|
||||||
sharedDirectory string
|
|
||||||
handler CommandClientHandler
|
handler CommandClientHandler
|
||||||
conn net.Conn
|
conn net.Conn
|
||||||
options CommandClientOptions
|
options CommandClientOptions
|
||||||
|
@ -29,25 +28,26 @@ type CommandClientHandler interface {
|
||||||
WriteGroups(message OutboundGroupIterator)
|
WriteGroups(message OutboundGroupIterator)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStandaloneCommandClient(sharedDirectory string) *CommandClient {
|
func NewStandaloneCommandClient() *CommandClient {
|
||||||
return &CommandClient{
|
return new(CommandClient)
|
||||||
sharedDirectory: sharedDirectory,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
|
func NewCommandClient(sharedDirectory string, handler CommandClientHandler, options *CommandClientOptions) *CommandClient {
|
||||||
return &CommandClient{
|
return &CommandClient{
|
||||||
sharedDirectory: sharedDirectory,
|
|
||||||
handler: handler,
|
handler: handler,
|
||||||
options: common.PtrValueOrDefault(options),
|
options: common.PtrValueOrDefault(options),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandClient) directConnect() (net.Conn, error) {
|
func (c *CommandClient) directConnect() (net.Conn, error) {
|
||||||
|
if !sTVOS {
|
||||||
return net.DialUnix("unix", nil, &net.UnixAddr{
|
return net.DialUnix("unix", nil, &net.UnixAddr{
|
||||||
Name: filepath.Join(c.sharedDirectory, "command.sock"),
|
Name: filepath.Join(sBasePath, "command.sock"),
|
||||||
Net: "unix",
|
Net: "unix",
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
return net.Dial("tcp", "127.0.0.1:8964")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *CommandClient) Connect() error {
|
func (c *CommandClient) Connect() error {
|
||||||
|
|
|
@ -18,7 +18,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type CommandServer struct {
|
type CommandServer struct {
|
||||||
sockPath string
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
handler CommandServerHandler
|
handler CommandServerHandler
|
||||||
|
|
||||||
|
@ -37,9 +36,8 @@ type CommandServerHandler interface {
|
||||||
ServiceReload() error
|
ServiceReload() error
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommandServer(sharedDirectory string, handler CommandServerHandler, maxLines int32) *CommandServer {
|
func NewCommandServer(handler CommandServerHandler, maxLines int32) *CommandServer {
|
||||||
server := &CommandServer{
|
server := &CommandServer{
|
||||||
sockPath: filepath.Join(sharedDirectory, "command.sock"),
|
|
||||||
handler: handler,
|
handler: handler,
|
||||||
savedLines: new(list.List[string]),
|
savedLines: new(list.List[string]),
|
||||||
maxLines: int(maxLines),
|
maxLines: int(maxLines),
|
||||||
|
@ -70,20 +68,29 @@ func (s *CommandServer) notifyURLTestUpdate() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *CommandServer) Start() error {
|
func (s *CommandServer) Start() error {
|
||||||
os.Remove(s.sockPath)
|
if !sTVOS {
|
||||||
|
return s.listenUNIX()
|
||||||
|
} else {
|
||||||
|
return s.listenTCP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CommandServer) listenUNIX() error {
|
||||||
|
sockPath := filepath.Join(sBasePath, "command.sock")
|
||||||
|
os.Remove(sockPath)
|
||||||
listener, err := net.ListenUnix("unix", &net.UnixAddr{
|
listener, err := net.ListenUnix("unix", &net.UnixAddr{
|
||||||
Name: s.sockPath,
|
Name: sockPath,
|
||||||
Net: "unix",
|
Net: "unix",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return E.Cause(err, "listen")
|
||||||
}
|
}
|
||||||
if sUserID > 0 {
|
if sUserID > 0 {
|
||||||
err = os.Chown(s.sockPath, sUserID, sGroupID)
|
err = os.Chown(sockPath, sUserID, sGroupID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
listener.Close()
|
listener.Close()
|
||||||
os.Remove(s.sockPath)
|
os.Remove(sockPath)
|
||||||
return err
|
return E.Cause(err, "chown")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.listener = listener
|
s.listener = listener
|
||||||
|
@ -91,6 +98,16 @@ func (s *CommandServer) Start() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *CommandServer) listenTCP() error {
|
||||||
|
listener, err := net.Listen("tcp", "127.0.0.1:8964")
|
||||||
|
if err != nil {
|
||||||
|
return E.Cause(err, "listen")
|
||||||
|
}
|
||||||
|
s.listener = listener
|
||||||
|
go s.loopConnection(listener)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *CommandServer) Close() error {
|
func (s *CommandServer) Close() error {
|
||||||
return common.Close(
|
return common.Close(
|
||||||
s.listener,
|
s.listener,
|
||||||
|
|
219
experimental/libbox/profile_import.go
Normal file
219
experimental/libbox/profile_import.go
Normal file
|
@ -0,0 +1,219 @@
|
||||||
|
package libbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
"github.com/sagernet/sing/common/rw"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncodeChunkedMessage(data []byte) []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
binary.Write(&buffer, binary.BigEndian, uint16(len(data)))
|
||||||
|
buffer.Write(data)
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeLengthChunk(data []byte) int32 {
|
||||||
|
return int32(binary.BigEndian.Uint16(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
MessageTypeError = iota
|
||||||
|
MessageTypeProfileList
|
||||||
|
MessageTypeProfileContentRequest
|
||||||
|
MessageTypeProfileContent
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrorMessage struct {
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ErrorMessage) Encode() []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteByte(MessageTypeError)
|
||||||
|
rw.WriteVString(&buffer, e.Message)
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeErrorMessage(data []byte) (*ErrorMessage, error) {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
messageType, err := rw.ReadByte(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if messageType != MessageTypeError {
|
||||||
|
return nil, E.New("invalid message")
|
||||||
|
}
|
||||||
|
var message ErrorMessage
|
||||||
|
message.Message, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProfileTypeLocal int32 = iota
|
||||||
|
ProfileTypeiCloud
|
||||||
|
ProfileTypeRemote
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProfilePreview struct {
|
||||||
|
ProfileID int64
|
||||||
|
Name string
|
||||||
|
Type int32
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfilePreviewIterator interface {
|
||||||
|
Next() *ProfilePreview
|
||||||
|
HasNext() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileEncoder struct {
|
||||||
|
profiles []ProfilePreview
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ProfileEncoder) Append(profile *ProfilePreview) {
|
||||||
|
e.profiles = append(e.profiles, *profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ProfileEncoder) Encode() []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteByte(MessageTypeProfileList)
|
||||||
|
binary.Write(&buffer, binary.BigEndian, uint16(len(e.profiles)))
|
||||||
|
for _, preview := range e.profiles {
|
||||||
|
binary.Write(&buffer, binary.BigEndian, preview.ProfileID)
|
||||||
|
rw.WriteVString(&buffer, preview.Name)
|
||||||
|
binary.Write(&buffer, binary.BigEndian, preview.Type)
|
||||||
|
}
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileDecoder struct {
|
||||||
|
profiles []*ProfilePreview
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProfileDecoder) Decode(data []byte) error {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
messageType, err := reader.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if messageType != MessageTypeProfileList {
|
||||||
|
return E.New("invalid message")
|
||||||
|
}
|
||||||
|
var profileCount uint16
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &profileCount)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i := 0; i < int(profileCount); i++ {
|
||||||
|
var profile ProfilePreview
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &profile.ProfileID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
profile.Name, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &profile.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.profiles = append(d.profiles, &profile)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *ProfileDecoder) Iterator() ProfilePreviewIterator {
|
||||||
|
return newIterator(d.profiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileContentRequest struct {
|
||||||
|
ProfileID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ProfileContentRequest) Encode() []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteByte(MessageTypeProfileContentRequest)
|
||||||
|
binary.Write(&buffer, binary.BigEndian, r.ProfileID)
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeProfileContentRequest(data []byte) (*ProfileContentRequest, error) {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
messageType, err := rw.ReadByte(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if messageType != MessageTypeProfileContentRequest {
|
||||||
|
return nil, E.New("invalid message")
|
||||||
|
}
|
||||||
|
var request ProfileContentRequest
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &request.ProfileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &request, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProfileContent struct {
|
||||||
|
Name string
|
||||||
|
Type int32
|
||||||
|
Config string
|
||||||
|
RemotePath string
|
||||||
|
AutoUpdate bool
|
||||||
|
LastUpdated int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ProfileContent) Encode() []byte {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
buffer.WriteByte(MessageTypeProfileContent)
|
||||||
|
rw.WriteVString(&buffer, c.Name)
|
||||||
|
binary.Write(&buffer, binary.BigEndian, c.Type)
|
||||||
|
rw.WriteVString(&buffer, c.Config)
|
||||||
|
rw.WriteVString(&buffer, c.RemotePath)
|
||||||
|
binary.Write(&buffer, binary.BigEndian, c.AutoUpdate)
|
||||||
|
binary.Write(&buffer, binary.BigEndian, c.LastUpdated)
|
||||||
|
return buffer.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecodeProfileContent(data []byte) (*ProfileContent, error) {
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
messageType, err := rw.ReadByte(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if messageType != MessageTypeProfileContent {
|
||||||
|
return nil, E.New("invalid message")
|
||||||
|
}
|
||||||
|
var content ProfileContent
|
||||||
|
content.Name, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &content.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content.Config, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
content.RemotePath, err = rw.ReadVString(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &content.AutoUpdate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = binary.Read(reader, binary.BigEndian, &content.LastUpdated)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &content, nil
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ func NewService(configContent string, platformInterface PlatformInterface) (*Box
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
ctx = filemanager.WithDefault(ctx, sBasePath, sTempPath, sUserID, sGroupID)
|
ctx = filemanager.WithDefault(ctx, sWorkingPath, sTempPath, sUserID, sGroupID)
|
||||||
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
|
ctx = service.ContextWithPtr(ctx, urltest.NewHistoryStorage())
|
||||||
instance, err := box.New(box.Options{
|
instance, err := box.New(box.Options{
|
||||||
Context: ctx,
|
Context: ctx,
|
||||||
|
|
|
@ -12,20 +12,25 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sBasePath string
|
sBasePath string
|
||||||
|
sWorkingPath string
|
||||||
sTempPath string
|
sTempPath string
|
||||||
sUserID int
|
sUserID int
|
||||||
sGroupID int
|
sGroupID int
|
||||||
|
sTVOS bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func Setup(basePath string, tempPath string) {
|
func Setup(basePath string, workingPath string, tempPath string, isTVOS bool) {
|
||||||
sBasePath = basePath
|
sBasePath = basePath
|
||||||
|
sWorkingPath = workingPath
|
||||||
sTempPath = tempPath
|
sTempPath = tempPath
|
||||||
sUserID = os.Getuid()
|
sUserID = os.Getuid()
|
||||||
sGroupID = os.Getgid()
|
sGroupID = os.Getgid()
|
||||||
|
sTVOS = isTVOS
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupWithUsername(basePath string, tempPath string, username string) error {
|
func SetupWithUsername(basePath string, workingPath string, tempPath string, username string) error {
|
||||||
sBasePath = basePath
|
sBasePath = basePath
|
||||||
|
sWorkingPath = workingPath
|
||||||
sTempPath = tempPath
|
sTempPath = tempPath
|
||||||
sUser, err := user.Lookup(username)
|
sUser, err := user.Lookup(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -21,7 +21,7 @@ require (
|
||||||
github.com/oschwald/maxminddb-golang v1.11.0
|
github.com/oschwald/maxminddb-golang v1.11.0
|
||||||
github.com/pires/go-proxyproto v0.7.0
|
github.com/pires/go-proxyproto v0.7.0
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
|
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0
|
||||||
github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182
|
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2
|
||||||
github.com/sagernet/quic-go v0.0.0-20230615020047-10f05c797c02
|
github.com/sagernet/quic-go v0.0.0-20230615020047-10f05c797c02
|
||||||
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -105,8 +105,8 @@ github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0 h1:KyhtFFt
|
||||||
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
|
github.com/sagernet/cloudflare-tls v0.0.0-20221031050923-d70792f4c3a0/go.mod h1:D4SFEOkJK+4W1v86ZhX0jPM0rAL498fyQAChqMtes/I=
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
|
||||||
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
|
||||||
github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182 h1:sD5g92IO15RAX2DvA4Cq3Uc7tcgqNWVi8K3VTCI6sEo=
|
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59 h1:vN4divY6LYHcYmiTsCHNPmGZtEsEKJzh81LyvgAQfEQ=
|
||||||
github.com/sagernet/gomobile v0.0.0-20230701084532-493ee2e45182/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
github.com/sagernet/gomobile v0.0.0-20230728014906-3de089147f59/go.mod h1:5YE39YkJkCcMsfq1jMKkjsrM2GfBoF9JVWnvU89hmvU=
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo=
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2 h1:dnkKrzapqtAwjTSWt6hdPrARORfoYvuUczynvRLrueo=
|
||||||
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
github.com/sagernet/gvisor v0.0.0-20230627031050-1ab0276e0dd2/go.mod h1:1JUiV7nGuf++YFm9eWZ8q2lrwHmhcUGzptMl/vL1+LA=
|
||||||
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
|
||||||
|
|
Loading…
Reference in a new issue