Files
KKRainbow 3d0c9c3ca5 chore(go): use lowercase module import path (#2560)
* chore(go): use lowercase module import path
* ci: scope checks for Go module changes
2026-09-10 15:13:55 +08:00

365 lines
8.8 KiB
Go

// Command dial-forward-bench measures the public Instance.Dial data path.
//
// User-facing port forwarding belongs to the embedded EasyTier core. This
// command intentionally keeps the forwarding loop in Go so benchmarks include
// the public Go/WASM data-plane boundary.
package main
import (
"context"
"errors"
"flag"
"fmt"
"io"
"log"
"net"
"net/netip"
"os"
"os/signal"
"strings"
"sync/atomic"
"syscall"
corehost "github.com/easytier/easytier/easytier-go"
"github.com/easytier/easytier/easytier-go/internal/contextutil"
)
type options struct {
peers peerList
portForwards portForwardList
networkName string
networkSecret string
ipv4 string
}
func main() {
options, err := parseOptions(os.Args[1:])
if err == flag.ErrHelp {
return
}
if err != nil {
log.Printf("invalid arguments: %v", err)
os.Exit(2)
}
ctx, stop := signal.NotifyContext(
context.Background(),
os.Interrupt,
syscall.SIGTERM,
)
defer stop()
if err := run(ctx, options); err != nil {
log.Printf("EasyTier Dial benchmark failed: %v", err)
os.Exit(1)
}
}
func run(ctx context.Context, options options) error {
prefix, err := netip.ParsePrefix(options.ipv4)
if err != nil {
return fmt.Errorf("parse instance IPv4 prefix: %w", err)
}
config, err := corehost.NewInstanceConfigBuilder(options.networkName).
NetworkSecret(options.networkSecret).
IPv4(prefix).
AddPeers([]string(options.peers)...).
Build()
if err != nil {
return fmt.Errorf("build EasyTier instance config: %w", err)
}
host, err := corehost.New(ctx, corehost.Options{})
if err != nil {
return fmt.Errorf("create EasyTier host: %w", err)
}
defer host.Close(context.Background())
instance, err := host.CreateInstance(ctx, config)
if err != nil {
return fmt.Errorf("create EasyTier instance: %w", err)
}
defer instance.Close(context.Background())
if err := instance.Start(ctx); err != nil {
return fmt.Errorf("start EasyTier instance: %w", err)
}
forwarders, err := startPortForwards(
ctx,
instance.Dial,
options.portForwards,
)
if err != nil {
return err
}
defer forwarders.Close()
<-ctx.Done()
return nil
}
func parseOptions(arguments []string) (options, error) {
flags := flag.NewFlagSet("dial-forward-bench", flag.ContinueOnError)
var options options
flags.Var(&options.peers, "p", "EasyTier peer URI; may be repeated")
flags.Var(
&options.portForwards,
"port-forward",
"tcp://bind/overlay-target or udp://bind/overlay-target; may be repeated",
)
flags.StringVar(&options.networkName, "network-name", "", "EasyTier network name")
flags.StringVar(&options.networkSecret, "network-secret", "", "EasyTier network secret")
flags.StringVar(&options.ipv4, "ipv4", "", "fixed EasyTier IPv4 address and prefix")
if err := flags.Parse(arguments); err != nil {
return options, err
}
if flags.NArg() != 0 {
return options, fmt.Errorf("unexpected argument %q", flags.Arg(0))
}
if err := options.validate(); err != nil {
return options, err
}
return options, nil
}
func (options options) validate() error {
if len(options.peers) == 0 {
return fmt.Errorf("at least one -p peer is required")
}
if len(options.portForwards) == 0 {
return fmt.Errorf("at least one --port-forward rule is required")
}
if strings.TrimSpace(options.networkName) == "" {
return fmt.Errorf("--network-name is required")
}
if options.networkSecret == "" {
return fmt.Errorf("--network-secret is required")
}
prefix, err := netip.ParsePrefix(options.ipv4)
if err != nil {
return fmt.Errorf("parse --ipv4: %w", err)
}
if !prefix.Addr().Is4() {
return fmt.Errorf("--ipv4 must be an IPv4 prefix")
}
return nil
}
type peerList []string
func (peers *peerList) Set(value string) error {
if strings.TrimSpace(value) == "" {
return fmt.Errorf("peer URI must not be empty")
}
*peers = append(*peers, value)
return nil
}
func (peers *peerList) String() string {
return strings.Join(*peers, ",")
}
type portForwardRule struct {
protocol string
bind netip.AddrPort
target netip.AddrPort
}
func (rule portForwardRule) String() string {
return fmt.Sprintf("%s://%s/%s", rule.protocol, rule.bind, rule.target)
}
type portForwardList []portForwardRule
func (rules *portForwardList) Set(value string) error {
rule, err := parsePortForwardRule(value)
if err == nil {
*rules = append(*rules, rule)
}
return err
}
func (rules *portForwardList) String() string {
values := make([]string, len(*rules))
for index, rule := range *rules {
values[index] = rule.String()
}
return strings.Join(values, ",")
}
func parsePortForwardRule(value string) (portForwardRule, error) {
protocol, addresses, ok := strings.Cut(value, "://")
if !ok || (protocol != "tcp" && protocol != "udp") {
return portForwardRule{}, fmt.Errorf(
"port forward %q must use tcp:// or udp://",
value,
)
}
bindText, targetText, ok := strings.Cut(addresses, "/")
if !ok {
return portForwardRule{}, fmt.Errorf(
"port forward %q must contain bind/overlay-target",
value,
)
}
bind, err := netip.ParseAddrPort(bindText)
if err != nil {
return portForwardRule{}, fmt.Errorf("parse bind address: %w", err)
}
target, err := netip.ParseAddrPort(targetText)
if err != nil {
return portForwardRule{}, fmt.Errorf("parse target address: %w", err)
}
if !bind.Addr().Is4() || !target.Addr().Is4() {
return portForwardRule{}, fmt.Errorf("port forward requires IPv4 addresses")
}
return portForwardRule{protocol: protocol, bind: bind, target: target}, nil
}
type overlayDialFunc func(context.Context, string, string) (net.Conn, error)
type portForwardSet struct {
ctx context.Context
cancel context.CancelFunc
dial overlayDialFunc
addresses []net.Addr
closers []io.Closer
}
func startPortForwards(
ctx context.Context,
dial overlayDialFunc,
rules []portForwardRule,
) (*portForwardSet, error) {
if dial == nil {
return nil, fmt.Errorf("port forward dial function is nil")
}
forwardContext, cancel := context.WithCancel(ctx)
forwards := &portForwardSet{
ctx: forwardContext,
cancel: cancel,
dial: dial,
}
for _, rule := range rules {
var err error
switch rule.protocol {
case "tcp":
err = forwards.startTCP(rule)
case "udp":
err = forwards.startUDP(rule)
default:
err = fmt.Errorf("unsupported protocol %q", rule.protocol)
}
if err != nil {
_ = forwards.Close()
return nil, fmt.Errorf("start port forward %s: %w", rule, err)
}
}
return forwards, nil
}
func (forwards *portForwardSet) Close() error {
forwards.cancel()
var errs []error
for _, closer := range forwards.closers {
errs = append(errs, closer.Close())
}
return errors.Join(errs...)
}
func (forwards *portForwardSet) startTCP(rule portForwardRule) error {
listener, err := net.ListenTCP("tcp4", net.TCPAddrFromAddrPort(rule.bind))
if err != nil {
return err
}
forwards.closers = append(forwards.closers, listener)
forwards.addresses = append(forwards.addresses, listener.Addr())
log.Printf("forwarding tcp://%s to %s through Instance.Dial", listener.Addr(), rule.target)
go func() {
for {
local, err := listener.Accept()
if err != nil {
return
}
go forwards.forwardTCP(local, rule.target)
}
}()
return nil
}
func (forwards *portForwardSet) forwardTCP(
local net.Conn,
target netip.AddrPort,
) {
defer local.Close()
overlay, err := forwards.dial(
forwards.ctx,
"tcp4",
target.String(),
)
if err != nil {
return
}
defer overlay.Close()
stopClose := contextutil.AfterFunc(forwards.ctx, func() {
_ = local.Close()
_ = overlay.Close()
})
defer stopClose()
done := make(chan struct{}, 2)
go func() { _, _ = io.Copy(overlay, local); done <- struct{}{} }()
go func() { _, _ = io.Copy(local, overlay); done <- struct{}{} }()
<-done
}
func (forwards *portForwardSet) startUDP(rule portForwardRule) error {
overlay, err := forwards.dial(
forwards.ctx,
"udp4",
rule.target.String(),
)
if err != nil {
return err
}
listener, err := net.ListenUDP("udp4", net.UDPAddrFromAddrPort(rule.bind))
if err != nil {
_ = overlay.Close()
return err
}
forwards.closers = append(forwards.closers, listener, overlay)
forwards.addresses = append(forwards.addresses, listener.LocalAddr())
log.Printf(
"forwarding udp://%s to %s through Instance.Dial",
listener.LocalAddr(),
rule.target,
)
var lastClient atomic.Value
go func() {
packet := make([]byte, 65535)
for {
length, client, err := listener.ReadFromUDPAddrPort(packet)
if err != nil {
return
}
lastClient.Store(client)
if written, err := overlay.Write(packet[:length]); err != nil ||
written != length {
return
}
}
}()
go func() {
packet := make([]byte, 65535)
for {
length, err := overlay.Read(packet)
if err != nil {
return
}
client, ok := lastClient.Load().(netip.AddrPort)
if ok {
_, _ = listener.WriteToUDPAddrPort(packet[:length], client)
}
}
}()
return nil
}