diff --git a/README.md b/README.md index d8b47ed..25186be 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,20 @@ Linux only. Go 1.22. ## Build 按目标架构分别编译。 + mkdir -p bin + go build -o bin/server ./cmd/server + go build -o bin/peer ./cmd/peer + go build -o bin/latencysummary ./cmd/latencysummary ### Linux amd64 ```bash CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/server-linux-amd64 ./cmd/server -CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/peer-linux-amd64 ./cmd/peer ``` ### Linux arm64 ```bash -CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/server-linux-arm64 ./cmd/server CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/peer-linux-arm64 ./cmd/peer ``` diff --git a/cmd/internal/peer/client.go b/cmd/internal/peer/client.go index 1c586fa..48cb5fa 100644 --- a/cmd/internal/peer/client.go +++ b/cmd/internal/peer/client.go @@ -6,16 +6,19 @@ import ( "os" "path/filepath" "sync/atomic" + "syscall" "omnisocketgo/cmd/internal/latencylog" "omnisocketgo/cmd/internal/protocol" "omnisocketgo/cmd/internal/transport" ) -var dialServer = net.Dial +var dialServer = dialServerWithOptions type clientOptions struct { - logger latencylog.Logger + logger latencylog.Logger + bindIP string + bindDevice string } // Option 用于配置 Client 的可选行为,例如时延日志。 @@ -28,6 +31,20 @@ func WithLogger(logger latencylog.Logger) Option { } } +// WithBindIP 指定拨号时使用的本地源 IP。 +func WithBindIP(ip string) Option { + return func(options *clientOptions) { + options.bindIP = ip + } +} + +// WithBindDevice 指定拨号时绑定的 Linux 网卡名,例如 eth0 或 wwan0。 +func WithBindDevice(device string) Option { + return func(options *clientOptions) { + options.bindDevice = device + } +} + // Client 表示一个已经连接到 server 的 peer。 type Client struct { id string @@ -49,7 +66,7 @@ func Dial(serverAddr, peerID string, opts ...Option) (*Client, error) { options.logger = latencylog.NoopLogger{} } - rawConn, err := dialServer("tcp", serverAddr) //使用 net.Dial 连接到 serverAddr 指定的 TCP 地址,返回一个 net.Conn。 + rawConn, err := dialServer(serverAddr, options) if err != nil { return nil, fmt.Errorf("peer: dial server: %w", err) } @@ -177,3 +194,42 @@ func (c *Client) Close() error { func (c *Client) nextMessageID() uint64 { return atomic.AddUint64(&c.nextID, 1) } + +func dialServerWithOptions(serverAddr string, options clientOptions) (net.Conn, error) { + dialer, err := buildDialer(options) + if err != nil { + return nil, err + } + + return dialer.Dial("tcp", serverAddr) +} + +func buildDialer(options clientOptions) (*net.Dialer, error) { + dialer := &net.Dialer{} + + if options.bindIP != "" { + ip := net.ParseIP(options.bindIP) + if ip == nil { + return nil, fmt.Errorf("peer: invalid bind ip %q", options.bindIP) + } + dialer.LocalAddr = &net.TCPAddr{IP: ip} + } + + if options.bindDevice != "" { + device := options.bindDevice + dialer.Control = func(_, _ string, rawConn syscall.RawConn) error { + var bindErr error + if err := rawConn.Control(func(fd uintptr) { + bindErr = syscall.BindToDevice(int(fd), device) + }); err != nil { + return err + } + if bindErr != nil { + return fmt.Errorf("peer: bind device %s: %w", device, bindErr) + } + return nil + } + } + + return dialer, nil +} diff --git a/cmd/internal/peer/client_test.go b/cmd/internal/peer/client_test.go index cdf7cef..0a34a7f 100644 --- a/cmd/internal/peer/client_test.go +++ b/cmd/internal/peer/client_test.go @@ -58,6 +58,51 @@ func TestDialRegistersPeer(t *testing.T) { waitFor(t, func() bool { return hub.HasPeer("peer-a") }, "peer-a to be registered") } +func TestDialRegistersPeerWithBindIP(t *testing.T) { + hub := server.NewHub() + cleanup := stubDialToHub(t, hub) + defer cleanup() + + client, err := Dial("ignored", "peer-a", WithBindIP("127.0.0.1")) + if err != nil { + t.Fatalf("Dial() with bind ip error = %v", err) + } + defer func() { _ = client.Close() }() + + waitFor(t, func() bool { return hub.HasPeer("peer-a") }, "peer-a to be registered") +} + +func TestDialRejectsInvalidBindIP(t *testing.T) { + _, err := Dial("ignored", "peer-a", WithBindIP("not-an-ip")) + if err == nil { + t.Fatal("Dial() error = nil, want invalid bind ip error") + } + if !strings.Contains(err.Error(), `invalid bind ip "not-an-ip"`) { + t.Fatalf("Dial() error = %v, want invalid bind ip error", err) + } +} + +func TestDialPassesBindDeviceOptionToDialer(t *testing.T) { + originalDial := dialServer + defer func() { + dialServer = originalDial + }() + + gotDevice := "" + dialServer = func(_ string, options clientOptions) (net.Conn, error) { + gotDevice = options.bindDevice + return nil, net.ErrClosed + } + + _, err := Dial("ignored", "peer-a", WithBindDevice("wwan0")) + if err == nil { + t.Fatal("Dial() error = nil, want dial error") + } + if gotDevice != "wwan0" { + t.Fatalf("bind device = %q, want %q", gotDevice, "wwan0") + } +} + func TestClientsExchangeTextAndFileMessages(t *testing.T) { hub := server.NewHub() cleanup := stubDialToHub(t, hub) @@ -662,8 +707,12 @@ func stubDialToHub(t *testing.T, hub *server.Hub) func() { originalDial := dialServer serverAddr, cleanup := startRealHubServer(t, hub) - dialServer = func(network, addr string) (net.Conn, error) { - return net.Dial(network, serverAddr) + dialServer = func(_ string, options clientOptions) (net.Conn, error) { + dialer, err := buildDialer(options) + if err != nil { + return nil, err + } + return dialer.Dial("tcp", serverAddr) } return func() { diff --git a/cmd/peer/main.go b/cmd/peer/main.go index ab3244b..4c9643c 100644 --- a/cmd/peer/main.go +++ b/cmd/peer/main.go @@ -19,12 +19,14 @@ func main() { targetPeer := flag.String("to", "", "optional target peer for one outgoing message") // 可选的目标 peer 标识 text := flag.String("text", "", "optional text to send after connecting") // 可选的文本消息内容 filePath := flag.String("file", "", "optional file path to send after connecting") + bindIP := flag.String("bind-ip", "", "optional local source IP used when dialing the server") + bindDevice := flag.String("bind-device", "", "optional Linux network device used when dialing the server") inboxDir := flag.String("inbox-dir", "inbox", "directory used to persist received text and file messages") logPath := flag.String("latency-log", "", "optional JSONL file path for latency timestamp logs") interactive := flag.Bool("interactive", true, "enable interactive REPL for repeated text/file sends on the same connection") flag.Parse() - clientOptions := make([]peerpkg.Option, 0, 1) + clientOptions := make([]peerpkg.Option, 0, 3) if *logPath != "" { logger, err := latencylog.NewJSONLLogger(*logPath) if err != nil { @@ -33,6 +35,12 @@ func main() { defer logger.Close() clientOptions = append(clientOptions, peerpkg.WithLogger(logger)) } + if *bindIP != "" { + clientOptions = append(clientOptions, peerpkg.WithBindIP(*bindIP)) + } + if *bindDevice != "" { + clientOptions = append(clientOptions, peerpkg.WithBindDevice(*bindDevice)) + } client, err := peerpkg.Dial(*serverAddr, *peerID, clientOptions...) if err != nil {