|
@@ -0,0 +1,286 @@
|
|
|
+package main
|
|
|
+
|
|
|
+import (
|
|
|
+ "context"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "net/netip"
|
|
|
+ "os"
|
|
|
+ "os/exec"
|
|
|
+ "strings"
|
|
|
+ "syscall"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ C "github.com/sagernet/sing-box/constant"
|
|
|
+ "github.com/sagernet/sing-box/include"
|
|
|
+ "github.com/sagernet/sing-box/log"
|
|
|
+ "github.com/sagernet/sing-box/option"
|
|
|
+ "github.com/sagernet/sing/common"
|
|
|
+ E "github.com/sagernet/sing/common/exceptions"
|
|
|
+ "github.com/sagernet/sing/common/json"
|
|
|
+ "github.com/sagernet/sing/common/shell"
|
|
|
+)
|
|
|
+
|
|
|
+var iperf3Path string
|
|
|
+
|
|
|
+func main() {
|
|
|
+ err := main0()
|
|
|
+ if err != nil {
|
|
|
+ log.Fatal(err)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func main0() error {
|
|
|
+ err := shell.Exec("sudo", "ls").Run()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ results, err := runTests()
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ encoder := json.NewEncoder(os.Stdout)
|
|
|
+ encoder.SetIndent("", " ")
|
|
|
+ return encoder.Encode(results)
|
|
|
+}
|
|
|
+
|
|
|
+func runTests() ([]TestResult, error) {
|
|
|
+ boxPaths := []string{
|
|
|
+ //"/Users/sekai/Downloads/sing-box-1.11.15-darwin-arm64/sing-box",
|
|
|
+ //"/Users/sekai/Downloads/sing-box-1.11.15-linux-arm64/sing-box",
|
|
|
+ "./sing-box",
|
|
|
+ }
|
|
|
+ stacks := []string{
|
|
|
+ "gvisor",
|
|
|
+ "system",
|
|
|
+ }
|
|
|
+ mtus := []int{
|
|
|
+ // 1500,
|
|
|
+ // 4064,
|
|
|
+ // 16384,
|
|
|
+ 32768,
|
|
|
+ 49152,
|
|
|
+ 65535,
|
|
|
+ }
|
|
|
+ flagList := [][]string{
|
|
|
+ {},
|
|
|
+ }
|
|
|
+ var results []TestResult
|
|
|
+ for _, boxPath := range boxPaths {
|
|
|
+ for _, stack := range stacks {
|
|
|
+ for _, mtu := range mtus {
|
|
|
+ if strings.HasPrefix(boxPath, ".") {
|
|
|
+ for _, flags := range flagList {
|
|
|
+ result, err := testOnce(boxPath, stack, mtu, false, flags)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ results = append(results, *result)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ result, err := testOnce(boxPath, stack, mtu, false, nil)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ results = append(results, *result)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return results, nil
|
|
|
+}
|
|
|
+
|
|
|
+type TestResult struct {
|
|
|
+ BoxPath string `json:"box_path"`
|
|
|
+ Stack string `json:"stack"`
|
|
|
+ MTU int `json:"mtu"`
|
|
|
+ Flags []string `json:"flags"`
|
|
|
+ MultiThread bool `json:"multi_thread"`
|
|
|
+ UploadSpeed string `json:"upload_speed"`
|
|
|
+ DownloadSpeed string `json:"download_speed"`
|
|
|
+}
|
|
|
+
|
|
|
+func testOnce(boxPath string, stackName string, mtu int, multiThread bool, flags []string) (result *TestResult, err error) {
|
|
|
+ testAddress := netip.MustParseAddr("1.1.1.1")
|
|
|
+ testConfig := option.Options{
|
|
|
+ Inbounds: []option.Inbound{
|
|
|
+ {
|
|
|
+ Type: C.TypeTun,
|
|
|
+ Options: &option.TunInboundOptions{
|
|
|
+ Address: []netip.Prefix{netip.MustParsePrefix("172.18.0.1/30")},
|
|
|
+ AutoRoute: true,
|
|
|
+ MTU: uint32(mtu),
|
|
|
+ Stack: stackName,
|
|
|
+ RouteAddress: []netip.Prefix{netip.PrefixFrom(testAddress, testAddress.BitLen())},
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ Route: &option.RouteOptions{
|
|
|
+ Rules: []option.Rule{
|
|
|
+ {
|
|
|
+ Type: C.RuleTypeDefault,
|
|
|
+ DefaultOptions: option.DefaultRule{
|
|
|
+ RawDefaultRule: option.RawDefaultRule{
|
|
|
+ IPCIDR: []string{testAddress.String()},
|
|
|
+ },
|
|
|
+ RuleAction: option.RuleAction{
|
|
|
+ Action: C.RuleActionTypeRouteOptions,
|
|
|
+ RouteOptionsOptions: option.RouteOptionsActionOptions{
|
|
|
+ OverrideAddress: "127.0.0.1",
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ },
|
|
|
+ AutoDetectInterface: true,
|
|
|
+ },
|
|
|
+ }
|
|
|
+ ctx := include.Context(context.Background())
|
|
|
+ tempConfig, err := os.CreateTemp("", "tun-bench-*.json")
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ defer os.Remove(tempConfig.Name())
|
|
|
+ encoder := json.NewEncoderContext(ctx, tempConfig)
|
|
|
+ encoder.SetIndent("", " ")
|
|
|
+ err = encoder.Encode(testConfig)
|
|
|
+ if err != nil {
|
|
|
+ return nil, E.Cause(err, "encode test config")
|
|
|
+ }
|
|
|
+ tempConfig.Close()
|
|
|
+ var sudoArgs []string
|
|
|
+ if len(flags) > 0 {
|
|
|
+ sudoArgs = append(sudoArgs, "env")
|
|
|
+ for _, flag := range flags {
|
|
|
+ sudoArgs = append(sudoArgs, flag)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ sudoArgs = append(sudoArgs, boxPath, "run", "-c", tempConfig.Name())
|
|
|
+ boxProcess := shell.Exec("sudo", sudoArgs...)
|
|
|
+ boxProcess.Stdout = &stderrWriter{}
|
|
|
+ boxProcess.Stderr = io.Discard
|
|
|
+ err = boxProcess.Start()
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if C.IsDarwin {
|
|
|
+ iperf3Path, err = exec.LookPath("iperf3-darwin")
|
|
|
+ } else {
|
|
|
+ iperf3Path, err = exec.LookPath("iperf3")
|
|
|
+ }
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ serverProcess := shell.Exec(iperf3Path, "-s")
|
|
|
+ serverProcess.Stdout = io.Discard
|
|
|
+ serverProcess.Stderr = io.Discard
|
|
|
+ err = serverProcess.Start()
|
|
|
+ if err != nil {
|
|
|
+ return nil, E.Cause(err, "start iperf3 server")
|
|
|
+ }
|
|
|
+
|
|
|
+ time.Sleep(time.Second)
|
|
|
+
|
|
|
+ args := []string{"-c", testAddress.String(), "-t", "5"}
|
|
|
+ if multiThread {
|
|
|
+ args = append(args, "-P", "10")
|
|
|
+ }
|
|
|
+
|
|
|
+ uploadProcess := shell.Exec(iperf3Path, args...)
|
|
|
+ output, err := uploadProcess.Read()
|
|
|
+ if err != nil {
|
|
|
+ boxProcess.Process.Signal(syscall.SIGKILL)
|
|
|
+ serverProcess.Process.Signal(syscall.SIGKILL)
|
|
|
+ println(output)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ uploadResult := common.SubstringBeforeLast(output, "iperf Done.")
|
|
|
+ uploadResult = common.SubstringBeforeLast(uploadResult, "sender")
|
|
|
+ uploadResult = common.SubstringBeforeLast(uploadResult, "bits/sec")
|
|
|
+ uploadResult = common.SubstringAfterLast(uploadResult, "Bytes")
|
|
|
+ uploadResult = strings.ReplaceAll(uploadResult, " ", "")
|
|
|
+
|
|
|
+ result = &TestResult{
|
|
|
+ BoxPath: boxPath,
|
|
|
+ Stack: stackName,
|
|
|
+ MTU: mtu,
|
|
|
+ Flags: flags,
|
|
|
+ MultiThread: multiThread,
|
|
|
+ UploadSpeed: uploadResult,
|
|
|
+ }
|
|
|
+
|
|
|
+ downloadProcess := shell.Exec(iperf3Path, append(args, "-R")...)
|
|
|
+ output, err = downloadProcess.Read()
|
|
|
+ if err != nil {
|
|
|
+ boxProcess.Process.Signal(syscall.SIGKILL)
|
|
|
+ serverProcess.Process.Signal(syscall.SIGKILL)
|
|
|
+ println(output)
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ downloadResult := common.SubstringBeforeLast(output, "iperf Done.")
|
|
|
+ downloadResult = common.SubstringBeforeLast(downloadResult, "receiver")
|
|
|
+ downloadResult = common.SubstringBeforeLast(downloadResult, "bits/sec")
|
|
|
+ downloadResult = common.SubstringAfterLast(downloadResult, "Bytes")
|
|
|
+ downloadResult = strings.ReplaceAll(downloadResult, " ", "")
|
|
|
+
|
|
|
+ result.DownloadSpeed = downloadResult
|
|
|
+
|
|
|
+ printArgs := []any{boxPath, stackName, mtu, "upload", uploadResult, "download", downloadResult}
|
|
|
+ if len(flags) > 0 {
|
|
|
+ printArgs = append(printArgs, "flags", strings.Join(flags, " "))
|
|
|
+ }
|
|
|
+ if multiThread {
|
|
|
+ printArgs = append(printArgs, "(-P 10)")
|
|
|
+ }
|
|
|
+ fmt.Println(printArgs...)
|
|
|
+ err = boxProcess.Process.Signal(syscall.SIGTERM)
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ err = serverProcess.Process.Signal(syscall.SIGTERM)
|
|
|
+ if err != nil {
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ boxDone := make(chan struct{})
|
|
|
+ go func() {
|
|
|
+ boxProcess.Cmd.Wait()
|
|
|
+ close(boxDone)
|
|
|
+ }()
|
|
|
+
|
|
|
+ serverDone := make(chan struct{})
|
|
|
+ go func() {
|
|
|
+ serverProcess.Process.Wait()
|
|
|
+ close(serverDone)
|
|
|
+ }()
|
|
|
+
|
|
|
+ select {
|
|
|
+ case <-boxDone:
|
|
|
+ case <-time.After(2 * time.Second):
|
|
|
+ boxProcess.Process.Kill()
|
|
|
+ case <-time.After(4 * time.Second):
|
|
|
+ println("box process did not close!")
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ select {
|
|
|
+ case <-serverDone:
|
|
|
+ case <-time.After(2 * time.Second):
|
|
|
+ serverProcess.Process.Kill()
|
|
|
+ case <-time.After(4 * time.Second):
|
|
|
+ println("server process did not close!")
|
|
|
+ os.Exit(1)
|
|
|
+ }
|
|
|
+
|
|
|
+ return
|
|
|
+}
|
|
|
+
|
|
|
+type stderrWriter struct{}
|
|
|
+
|
|
|
+func (w *stderrWriter) Write(p []byte) (n int, err error) {
|
|
|
+ return os.Stderr.Write(p)
|
|
|
+}
|