mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-09-17 05:40:27 +00:00
203 lines
5.6 KiB
Go
203 lines
5.6 KiB
Go
//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
|
|
}
|