mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-10-27 05:26:56 +00:00
Add Taildrop support
This commit is contained in:
@@ -47,6 +47,7 @@ import (
|
||||
tailscaleroot "github.com/sagernet/tailscale"
|
||||
_ "github.com/sagernet/tailscale/feature/relayserver"
|
||||
"github.com/sagernet/tailscale/ipn"
|
||||
"github.com/sagernet/tailscale/ipn/ipnlocal"
|
||||
tsDNS "github.com/sagernet/tailscale/net/dns"
|
||||
"github.com/sagernet/tailscale/net/netmon"
|
||||
"github.com/sagernet/tailscale/net/netns"
|
||||
@@ -117,6 +118,8 @@ type Endpoint struct {
|
||||
|
||||
sshServerInstance *tailssh.Server
|
||||
sshServerOptions *option.TailscaleSSHServerOptions
|
||||
taildrop *taildropManager
|
||||
localBackend *ipnlocal.LocalBackend
|
||||
|
||||
systemInterface bool
|
||||
systemInterfaceName string
|
||||
@@ -181,6 +184,12 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
}
|
||||
dialerQueryOptions := outboundDialer.(dialer.ResolveDialer).QueryOptions()
|
||||
dnsRouter := service.FromContext[adapter.DNSRouter](ctx)
|
||||
taildropDirectory := options.TaildropDirectory
|
||||
if taildropDirectory == "" {
|
||||
taildropDirectory = "Taildrop"
|
||||
}
|
||||
taildropDirectory = filemanager.BasePath(ctx, os.ExpandEnv(taildropDirectory))
|
||||
taildropDirectory, _ = filepath.Abs(taildropDirectory)
|
||||
return &Endpoint{
|
||||
Adapter: endpoint.NewAdapter(C.TypeTailscale, tag, []string{N.NetworkTCP, N.NetworkUDP, N.NetworkICMP}, nil),
|
||||
ctx: ctx,
|
||||
@@ -231,6 +240,7 @@ func NewEndpoint(ctx context.Context, router adapter.Router, logger log.ContextL
|
||||
relayServerPort: options.RelayServerPort,
|
||||
relayServerStaticEndpoints: options.RelayServerStaticEndpoints,
|
||||
sshServerOptions: options.SSHServer,
|
||||
taildrop: newTaildropManager(ctx, logger, tag, taildropDirectory, platformInterface),
|
||||
udpTimeout: udpTimeout,
|
||||
icmpTimeout: C.ICMPTimeout,
|
||||
systemInterface: options.SystemInterface,
|
||||
@@ -247,6 +257,12 @@ func (t *Endpoint) Start(stage adapter.StartStage) error {
|
||||
if mkdirErr != nil {
|
||||
return E.Cause(mkdirErr, "create state directory")
|
||||
}
|
||||
if !version.IsAppleTV() {
|
||||
mkdirErr = filemanager.MkdirAll(t.ctx, t.taildrop.directory, 0o700)
|
||||
if mkdirErr != nil {
|
||||
return E.Cause(mkdirErr, "create taildrop directory")
|
||||
}
|
||||
}
|
||||
t.server.PeerDNSQueryHandler = (*peerDNSQueryHandler)(t)
|
||||
case adapter.StartStateStart:
|
||||
return t.start()
|
||||
@@ -400,7 +416,13 @@ func (t *Endpoint) postStart() error {
|
||||
}, true
|
||||
})
|
||||
}
|
||||
wgEngine := t.server.ExportLocalBackend().ExportEngine().(wgengine.ExportedUserspaceEngine)
|
||||
localBackend := t.server.ExportLocalBackend()
|
||||
t.localBackend = localBackend
|
||||
if !version.IsAppleTV() {
|
||||
registerTaildropEndpoint(localBackend, t)
|
||||
go t.taildrop.start()
|
||||
}
|
||||
wgEngine := localBackend.ExportEngine().(wgengine.ExportedUserspaceEngine)
|
||||
wgEngine.SetOnReconfigListener(t.onReconfig)
|
||||
t.wgEngine = wgEngine
|
||||
|
||||
@@ -689,6 +711,11 @@ func (t *Endpoint) Logout(ctx context.Context) error {
|
||||
func (t *Endpoint) Close() error {
|
||||
var err error
|
||||
t.started.Store(false)
|
||||
if t.localBackend != nil {
|
||||
unregisterTaildropEndpoint(t.localBackend)
|
||||
t.localBackend = nil
|
||||
}
|
||||
t.taildrop.close()
|
||||
if t.icmpForwarder != nil {
|
||||
t.icmpForwarder.Close()
|
||||
t.icmpForwarder = nil
|
||||
|
||||
@@ -48,6 +48,7 @@ func convertPingResult(result *ipnstate.PingResult) *adapter.TailscalePingResult
|
||||
LatencyMs: result.LatencySeconds * 1000,
|
||||
IsDirect: result.Endpoint != "",
|
||||
Endpoint: result.Endpoint,
|
||||
PeerRelay: result.PeerRelay,
|
||||
DERPRegionID: int32(result.DERPRegionID),
|
||||
DERPRegionCode: result.DERPRegionCode,
|
||||
Error: result.Err,
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build tvos
|
||||
|
||||
package tailscale
|
||||
|
||||
import "github.com/sagernet/tailscale/version"
|
||||
|
||||
func init() {
|
||||
version.SetAppleTV()
|
||||
}
|
||||
@@ -37,9 +37,36 @@ func (t *Endpoint) SubscribeTailscaleStatus(ctx context.Context, fn func(*adapte
|
||||
status := localBackend.Status()
|
||||
result := convertTailscaleStatus(status)
|
||||
result.KeyAuth = t.keyAuth
|
||||
canShareFiles, taildropTargets := t.taildropTargets()
|
||||
result.CanShareFiles = canShareFiles
|
||||
result.WaitingFileCount = t.taildrop.waitingFileCount()
|
||||
result.ReceivingFileCount = t.taildrop.receivingFileCount()
|
||||
result.UnreadFileCount = t.taildrop.unreadFileCount()
|
||||
if len(taildropTargets) > 0 {
|
||||
for _, group := range result.UserGroups {
|
||||
for _, peer := range group.Peers {
|
||||
peer.CanReceiveFiles = taildropTargets[peer.StableID]
|
||||
}
|
||||
}
|
||||
}
|
||||
fn(result)
|
||||
}
|
||||
}()
|
||||
fileSignal := make(chan struct{}, 1)
|
||||
watchErr := t.taildrop.watch(t.taildrop.fileWatchers, fileSignal)
|
||||
if watchErr == nil {
|
||||
defer t.taildrop.unwatch(t.taildrop.fileWatchers, fileSignal)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-fileSignal:
|
||||
scheduleUpdate()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
scheduleUpdate()
|
||||
for {
|
||||
var busError string
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
//go:build with_gvisor
|
||||
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sagernet/sing-box/experimental/locale"
|
||||
E "github.com/sagernet/sing/common/exceptions"
|
||||
M "github.com/sagernet/sing/common/metadata"
|
||||
N "github.com/sagernet/sing/common/network"
|
||||
"github.com/sagernet/tailscale/ipn"
|
||||
"github.com/sagernet/tailscale/tailcfg"
|
||||
)
|
||||
|
||||
func (t *Endpoint) SendTaildropFile(ctx context.Context, peerStableID string, fileName string, size int64, content io.Reader, progress func(sentBytes int64)) error {
|
||||
if !t.started.Load() {
|
||||
return E.New("Tailscale is not ready yet")
|
||||
}
|
||||
err := validateTaildropFileName(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localBackend := t.server.ExportLocalBackend()
|
||||
if localBackend.State() != ipn.Running {
|
||||
return E.New("taildrop: not connected to the tailnet")
|
||||
}
|
||||
nodeBackend := localBackend.NodeBackend()
|
||||
self := nodeBackend.Self()
|
||||
if !self.Valid() {
|
||||
return E.New("taildrop: not connected to the tailnet")
|
||||
}
|
||||
if !self.CapMap().Contains(tailcfg.CapabilityFileSharing) {
|
||||
return E.New("taildrop: file sharing not enabled by Tailscale admin")
|
||||
}
|
||||
var peer tailcfg.NodeView
|
||||
for _, candidate := range nodeBackend.Peers() {
|
||||
if string(candidate.StableID()) == peerStableID {
|
||||
peer = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
if !peer.Valid() {
|
||||
return E.New("taildrop: peer not found: ", peerStableID)
|
||||
}
|
||||
if peer.Hostinfo().OS() == "tvOS" {
|
||||
return E.New("taildrop: peer cannot receive files")
|
||||
}
|
||||
if self.User() != peer.User() && !nodeBackend.PeerHasCap(peer, tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
return E.New("taildrop: peer is not a permitted file target")
|
||||
}
|
||||
peerAPIBase := nodeBackend.PeerAPIBase(peer)
|
||||
if peerAPIBase == "" {
|
||||
return E.New("taildrop: peer does not support peer API")
|
||||
}
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network string, address string) (net.Conn, error) {
|
||||
return t.DialContext(ctx, N.NetworkTCP, M.ParseSocksaddr(address))
|
||||
},
|
||||
},
|
||||
}
|
||||
defer httpClient.CloseIdleConnections()
|
||||
fileURL := peerAPIBase + "/v0/put/" + url.PathEscape(fileName)
|
||||
offset, remaining := taildropResume(ctx, httpClient, fileURL, content)
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodPut, fileURL, &taildropProgressReader{
|
||||
reader: remaining,
|
||||
sent: offset,
|
||||
progress: progress,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if size >= 0 {
|
||||
request.ContentLength = size - offset
|
||||
}
|
||||
if offset > 0 {
|
||||
request.Header.Set("Range", "bytes="+strconv.FormatInt(offset, 10)+"-")
|
||||
}
|
||||
if progress != nil {
|
||||
progress(offset)
|
||||
}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return E.Cause(err, "taildrop: send ", fileName)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
messageBytes, _ := io.ReadAll(io.LimitReader(response.Body, 1024))
|
||||
message := strings.TrimSpace(string(messageBytes))
|
||||
if message == errTaildropCanceled.Error() {
|
||||
return E.New(fmt.Sprintf(locale.Current().TaildropSendCanceled, fileName))
|
||||
}
|
||||
return E.New("taildrop: send ", fileName, ": peer responded ", response.Status, ": ", message)
|
||||
}
|
||||
_, err = io.Copy(io.Discard, response.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func taildropResume(ctx context.Context, httpClient *http.Client, fileURL string, content io.Reader) (int64, io.Reader) {
|
||||
probeCtx, cancelProbe := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancelProbe()
|
||||
request, err := http.NewRequestWithContext(probeCtx, http.MethodGet, fileURL, nil)
|
||||
if err != nil {
|
||||
return 0, content
|
||||
}
|
||||
response, err := httpClient.Do(request)
|
||||
if err != nil {
|
||||
return 0, content
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return 0, content
|
||||
}
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
var offset int64
|
||||
block := make([]byte, 0, taildropBlockSize)
|
||||
for {
|
||||
var remoteChecksum taildropBlockChecksum
|
||||
err = decoder.Decode(&remoteChecksum)
|
||||
if err != nil || remoteChecksum.Algorithm != "sha256" || remoteChecksum.Size < 0 || remoteChecksum.Size > taildropBlockSize {
|
||||
break
|
||||
}
|
||||
var n int
|
||||
n, err = io.ReadFull(content, block[:remoteChecksum.Size])
|
||||
block = block[:n]
|
||||
if n == 0 || (err != nil && err != io.EOF && err != io.ErrUnexpectedEOF) {
|
||||
break
|
||||
}
|
||||
localSum := sha256.Sum256(block)
|
||||
if hex.EncodeToString(localSum[:]) != remoteChecksum.Checksum {
|
||||
break
|
||||
}
|
||||
offset += int64(n)
|
||||
block = block[:0]
|
||||
}
|
||||
if len(block) > 0 {
|
||||
return offset, io.MultiReader(bytes.NewReader(block), content)
|
||||
}
|
||||
return offset, content
|
||||
}
|
||||
|
||||
type taildropProgressReader struct {
|
||||
reader io.Reader
|
||||
sent int64
|
||||
progress func(sentBytes int64)
|
||||
}
|
||||
|
||||
func (r *taildropProgressReader) Read(buffer []byte) (int, error) {
|
||||
n, err := r.reader.Read(buffer)
|
||||
if n > 0 {
|
||||
r.sent += int64(n)
|
||||
if r.progress != nil {
|
||||
r.progress(r.sent)
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t *Endpoint) taildropTargets() (canShareFiles bool, targets map[string]bool) {
|
||||
if !t.started.Load() {
|
||||
return false, nil
|
||||
}
|
||||
localBackend := t.server.ExportLocalBackend()
|
||||
if localBackend.State() != ipn.Running {
|
||||
return false, nil
|
||||
}
|
||||
nodeBackend := localBackend.NodeBackend()
|
||||
self := nodeBackend.Self()
|
||||
if !self.Valid() || !self.CapMap().Contains(tailcfg.CapabilityFileSharing) {
|
||||
return false, nil
|
||||
}
|
||||
targets = make(map[string]bool)
|
||||
for _, peer := range nodeBackend.Peers() {
|
||||
if !peer.Valid() || peer.Hostinfo().OS() == "tvOS" {
|
||||
continue
|
||||
}
|
||||
if self.User() != peer.User() && !nodeBackend.PeerHasCap(peer, tailcfg.PeerCapabilityFileSharingTarget) {
|
||||
continue
|
||||
}
|
||||
if !nodeBackend.PeerHasPeerAPI(peer) {
|
||||
continue
|
||||
}
|
||||
targets[string(peer.StableID())] = true
|
||||
}
|
||||
return true, targets
|
||||
}
|
||||
Reference in New Issue
Block a user