mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-10-08 19:06:14 -08:00
* chore(go): use lowercase module import path * ci: scope checks for Go module changes
365 lines
8.8 KiB
Go
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
|
|
}
|