From 23d9238464201081428ce2fd03c165bc08bd4bb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:02:45 +0800 Subject: [PATCH] feat(p2p): add ICMP backend support --- README.md | 8 +- README_ZH.md | 8 +- go.mod | 7 +- go.sum | 10 +- gold/link/me.go | 5 +- gold/link/peer.go | 4 +- gold/link/send.go | 8 +- gold/p2p/define.go | 2 +- gold/p2p/icmp/icmp.go | 264 ++++++++++++ gold/p2p/icmp/init.go | 26 ++ gold/p2p/ip/ip.go | 5 +- gold/p2p/tcp/tcp.go | 2 +- gold/p2p/udp/udp.go | 5 +- gold/p2p/udplite/udp.go | 5 +- gold/proto/nat/nat.go | 2 +- upper/services/tunnel/tunnel.go | 1 + upper/services/tunnel/tunnel_icmp_test.go | 466 ++++++++++++++++++++++ upper/services/tunnel/tunnel_test.go | 8 +- upper/services/wg/wg.go | 1 + 19 files changed, 809 insertions(+), 28 deletions(-) create mode 100644 gold/p2p/icmp/icmp.go create mode 100644 gold/p2p/icmp/init.go create mode 100644 upper/services/tunnel/tunnel_icmp_test.go diff --git a/README.md b/README.md index 5fbc3be..2161024 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ WireGold is a pure Go Layer 3 VPN inspired by WireGuard. ### Features - **Encryption**: XChaCha20-Poly1305 (AEAD) + Curve25519 key exchange + BLAKE2B integrity check -- **Transport**: UDP / UDP-Lite / TCP / Raw IP +- **Transport**: UDP / UDP-Lite / TCP / Raw IP / ICMP - **Encoding**: Optional Base16384 encoding to traverse text-only filters - **Anti-censorship**: XOR mask header obfuscation + randomized MTU scaling + optional double-send - **Compression**: Optional Zstd payload compression @@ -54,14 +54,17 @@ wg [-c config.yaml] [-d|w] [-g] [-h] [-p] [-l log.txt] - **macOS Mojave**: max MTU (IPv4 endpoint) is `9159` - **IPv6 endpoint**: recommended MTU `1280–1500` to avoid oversized segment drops +- **ICMP / Raw IP endpoint**: use bare IP address without port (e.g. `0.0.0.0`), requires root/admin privileges ```yaml IP: 192.168.233.1 SubNet: 192.168.233.0/24 PrivateKey: 暲菉斂狧污爉窫擸紈卆帞蔩慈睠庮扝憚瞼縀 +Network: udp # udp (default), udplite, tcp, ip, icmp EndPoint: 0.0.0.0:56789 MTU: 1504 SpeedLoop: 4096 +MaxTTL: 64 Mask: 0x1234567890abcdef Base14: true Peers: @@ -94,6 +97,9 @@ Peers: | Field | Description | |-------|-------------| +| `Network` | Transport protocol: `udp` (default), `udplite`, `tcp`, `ip`, `icmp` | +| `MaxTTL` | Initial TTL for outgoing packets; default `64` | +| `SpeedLoop` | Log receive throughput statistics every N packets; default `4096` | | `AllowedIPs` | Prefix `x` to accept packets from the subnet without creating a system route; prefix `y` to add an internal route table entry only | | `Mask` | XOR mask for header obfuscation | | `Base14` | Enable Base16384 encoding | diff --git a/README_ZH.md b/README_ZH.md index 3674db7..03e157b 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -15,7 +15,7 @@ WireGold 是一个纯 Go 实现的第 3 层 VPN,灵感来自 WireGuard。 ### 主要特性 - **加密**: XChaCha20-Poly1305 (AEAD) + Curve25519 密钥交换 + BLAKE2B 完整性校验 -- **传输**: 支持 UDP / UDP-Lite / TCP / Raw IP 多种底层传输 +- **传输**: 支持 UDP / UDP-Lite / TCP / Raw IP / ICMP 多种底层传输 - **编码**: 可选 Base16384 编码以穿越文本过滤 - **抗审查**: XOR 掩码混淆报头 + 随机 MTU 放缩 + 可选双倍发包 - **压缩**: 可选 Zstd 数据压缩 @@ -53,14 +53,17 @@ wg [-c config.yaml] [-d|w] [-g] [-h] [-p] [-l log.txt] - **macOS Mojave**: 最大 MTU (IPv4 endpoint) 为 `9159` - **IPv6 endpoint**: 推荐 MTU `1280~1500`,避免大分片被丢弃 +- **ICMP / Raw IP endpoint**: 使用裸 IP 地址,无需端口号 (如 `0.0.0.0`)。需要 root/管理员权限 ```yaml IP: 192.168.233.1 SubNet: 192.168.233.0/24 PrivateKey: 暲菉斂狧污爉窫擸紈卆帞蔩慈睠庮扝憚瞼縀 +Network: udp # udp (默认), udplite, tcp, ip, icmp EndPoint: 0.0.0.0:56789 MTU: 1504 SpeedLoop: 4096 +MaxTTL: 64 Mask: 0x1234567890abcdef Base14: true Peers: @@ -93,6 +96,9 @@ Peers: | 字段 | 说明 | |------|------| +| `Network` | 传输协议: `udp` (默认), `udplite`, `tcp`, `ip`, `icmp` | +| `MaxTTL` | 发包初始 TTL,默认 `64` | +| `SpeedLoop` | 每收到 N 个包时输出一次吞吐统计,默认 `4096` | | `AllowedIPs` | 前缀 `x` 表示只接受该网段报文但不建系统路由;前缀 `y` 表示只添加内部路由表条目 | | `Mask` | XOR 掩码,用于混淆报头 | | `Base14` | 启用 Base16384 编码 | diff --git a/go.mod b/go.mod index 1547e3b..bb76f1c 100644 --- a/go.mod +++ b/go.mod @@ -12,12 +12,13 @@ require ( github.com/fumiama/water v0.0.0-20211231134027-da391938d6ac github.com/klauspost/compress v1.18.5 github.com/sirupsen/logrus v1.9.4 - golang.org/x/crypto v0.49.0 + golang.org/x/crypto v0.50.0 + golang.org/x/net v0.53.0 + golang.org/x/sys v0.43.0 gopkg.in/yaml.v3 v3.0.1 ) require ( github.com/fumiama/wintun v0.0.0-20211229152851-8bc97c8034c0 // indirect - golang.org/x/sys v0.43.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/text v0.36.0 // indirect ) diff --git a/go.sum b/go.sum index 4c5675d..4a5ebb6 100644 --- a/go.sum +++ b/go.sum @@ -30,9 +30,11 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= @@ -40,8 +42,8 @@ golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/gold/link/me.go b/gold/link/me.go index 1d5fdda..1f2ff24 100644 --- a/gold/link/me.go +++ b/gold/link/me.go @@ -208,7 +208,10 @@ func (m *Me) NetworkConfigs() []any { func (m *Me) Close() error { for i := 0; i < len(m.jobs); i++ { - close(m.jobs[i]) + jb := m.jobs[i] + if jb != nil { + close(jb) + } } m.connections = nil if bin.IsNonNilInterface(m.conn) { diff --git a/gold/link/peer.go b/gold/link/peer.go index 479941e..5ef717e 100644 --- a/gold/link/peer.go +++ b/gold/link/peer.go @@ -137,8 +137,8 @@ func (m *Me) extractPeer(srcip, dstip net.IP, addr p2p.EndPoint) *Link { logrus.Warnln(file.Header(), "packet from", srcip, "to", dstip, "is refused") return nil } - if bin.IsNilInterface(p.endpoint) || !p.endpoint.Euqal(addr) { - if m.ep.Network() == "tcp" && !addr.Euqal(p.endpoint) { + if bin.IsNilInterface(p.endpoint) || !p.endpoint.Equal(addr) { + if m.ep.Network() == "tcp" && !addr.Equal(p.endpoint) { logrus.Infoln(file.Header(), "set endpoint of peer", p.peerip, "to", addr.String()) p.endpoint = addr } else { // others are all no status link diff --git a/gold/link/send.go b/gold/link/send.go index c683121..cc8db2f 100644 --- a/gold/link/send.go +++ b/gold/link/send.go @@ -75,16 +75,12 @@ func (l *Link) write2peer(b pbuf.Bytes, seq uint32) { if l.doublepacket { err := l.write2peer1(b, seq) if err != nil { - if config.ShowDebugLog { - logrus.Warnln("[send] double wr2peer", l.peerip, "err:", err) - } + logrus.Warnln("[send] double wr2peer", l.peerip, "err:", err) } } err := l.write2peer1(b, seq) if err != nil { - if config.ShowDebugLog { - logrus.Warnln("[send] wr2peer", l.peerip, "err:", err) - } + logrus.Warnln("[send] wr2peer", l.peerip, "err:", err) } } diff --git a/gold/p2p/define.go b/gold/p2p/define.go index 0c4aedf..a7bd31e 100644 --- a/gold/p2p/define.go +++ b/gold/p2p/define.go @@ -23,7 +23,7 @@ func Register(network string, initializer Initializer) (actual Initializer, hase type EndPoint interface { fmt.Stringer Network() string - Euqal(EndPoint) bool + Equal(EndPoint) bool Listen() (Conn, error) } diff --git a/gold/p2p/icmp/icmp.go b/gold/p2p/icmp/icmp.go new file mode 100644 index 0000000..2a36aa1 --- /dev/null +++ b/gold/p2p/icmp/icmp.go @@ -0,0 +1,264 @@ +package icmp + +import ( + "errors" + "net" + "net/netip" + "os" + "sync" + "sync/atomic" + + "github.com/RomiChan/syncx" + "github.com/fumiama/WireGold/config" + "github.com/fumiama/WireGold/gold/p2p" + "github.com/fumiama/orbyte/pbuf" + "github.com/sirupsen/logrus" + + "golang.org/x/net/icmp" + "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" +) + +var ( + ErrInvalidBodyType = errors.New("invalid body type") +) + +var ( + echoid = os.Getpid() +) + +// peerState holds per-peer ICMP echo state within a Conn. +type peerState struct { + id int + seq atomic.Uintptr + seqpool *sync.Pool +} + +func newPeerState() *peerState { + ps := &peerState{} + ps.seqpool = &sync.Pool{ + New: func() any { + return int(ps.seq.Add(1)) + }, + } + return ps +} + +type EndPoint netip.Addr + +func (ep *EndPoint) String() string { + return (*netip.Addr)(ep).String() +} + +func (ep *EndPoint) Network() string { + return "icmp" +} + +func (ep *EndPoint) Equal(ep2 p2p.EndPoint) bool { + if ep == nil || ep2 == nil { + return ep == nil && ep2 == nil + } + ipep2, ok := ep2.(*EndPoint) + if !ok { + return false + } + ipep1 := ep + return (*netip.Addr)(ipep1).Compare(*(*netip.Addr)(ipep2)) == 0 +} + +// network get ipv4/ipv6 info and choose different options. +func (ep *EndPoint) network() (string, *netip.Addr) { + nw := "ip4:icmp" + if (*netip.Addr)(ep).Is6() { + nw = "ip6:ipv6-icmp" + } + return nw, (*netip.Addr)(ep) +} + +func (ep *EndPoint) Listen() (p2p.Conn, error) { + nw, addr := ep.network() + conn, err := icmp.ListenPacket(nw, addr.String()) + if err != nil { + return nil, err + } + return &Conn{inner: conn}, nil +} + +type Conn struct { + inner *icmp.PacketConn + peers syncx.Map[netip.Addr, *peerState] +} + +func (conn *Conn) getOrCreatePeerState(addr netip.Addr) *peerState { + if ps, ok := conn.peers.Load(addr); ok { + return ps + } + ps := newPeerState() + actual, _ := conn.peers.LoadOrStore(addr, ps) + return actual +} + +func (conn *Conn) Close() error { + return conn.inner.Close() +} + +func (conn *Conn) String() string { + return conn.inner.LocalAddr().String() +} + +func (conn *Conn) LocalAddr() p2p.EndPoint { + eps := conn.inner.LocalAddr().String() + addr, err := netip.ParseAddrPort(eps) + if err == nil { + eps = addr.Addr().String() + } + ep, _ := NewEndpoint(eps) + return ep +} + +func (conn *Conn) ReadFromPeer(b []byte) (n int, ep p2p.EndPoint, err error) { + buf := pbuf.NewBytes(8192) + defer buf.ManualDestroy() + var ipaddr netip.Addr + buf.V(func(data []byte) { + ok := false + var msg *icmp.Message + for !ok { + var ( + cnt int + addr net.Addr + ) + cnt, addr, err = conn.inner.ReadFrom(data) + if err != nil { + if config.ShowDebugLog { + logrus.Debugln("[icmp] recv ReadFrom err:", err) + } + return + } + ipaddr, err = netip.ParseAddr(addr.String()) + if err != nil { + if config.ShowDebugLog { + logrus.Debugln("[icmp] recv ParseAddr err:", err, ", addr:", addr) + } + return + } + ep, err = NewEndpoint(ipaddr.String()) + if err != nil { + if config.ShowDebugLog { + logrus.Debugln("[icmp] recv NewEndpoint err:", err, ", addr:", addr) + } + return + } + proton := ipv4.ICMPTypeEcho.Protocol() + if ipaddr.Is6() { + proton = ipv6.ICMPTypeEchoRequest.Protocol() + } + + msg, err = icmp.ParseMessage(proton, data[:cnt]) + if err != nil { + if config.ShowDebugLog { + logrus.Debugln("[icmp] recv ParseMessage err:", err, ", addr:", addr) + } + return + } + + ok = msg.Type == ipv4.ICMPTypeEcho || msg.Type == ipv4.ICMPTypeEchoReply + if ipaddr.Is6() { + ok = msg.Type == ipv6.ICMPTypeEchoRequest || msg.Type == ipv6.ICMPTypeEchoReply + } + ok = ok && msg.Code == 1 + if config.ShowDebugLog { + logrus.Debugln("[icmp] recv from", ipaddr, ", is valid:", ok) + } + } + body, okk := msg.Body.(*icmp.Echo) + if !okk { + err = ErrInvalidBodyType + return + } + if msg.Type == ipv4.ICMPTypeEcho || msg.Type == ipv6.ICMPTypeEchoRequest { + ps := conn.getOrCreatePeerState(ipaddr) + ps.id = body.ID + ps.seq.Store(uintptr(body.Seq)) + ps.seqpool.Put(body.Seq) + } + n = copy(b, body.Data) + if config.ShowDebugLog { + logrus.Debugln("[icmp] recv", n, "bytes data from", ipaddr) + } + }) + return +} + +func (conn *Conn) WriteToPeer(b []byte, ep p2p.EndPoint) (int, error) { + icmpep, ok := ep.(*EndPoint) + if !ok { + return 0, p2p.ErrEndpointTypeMistatch + } + addr := (*netip.Addr)(icmpep) + ps := conn.getOrCreatePeerState(*addr) + seq := ps.seqpool.Get().(int) + id := ps.id + isrequest := id == 0 + if isrequest { + id = echoid + } + var ( + ip net.IP + msg icmp.Message + ) + if addr.Is4() { + x := addr.As4() + ip = x[:] + msg = icmp.Message{ + Code: 1, + Body: &icmp.Echo{ + ID: id, + Seq: seq, + Data: b, + }, + } + if isrequest { + msg.Type = ipv4.ICMPTypeEcho + } else { + msg.Type = ipv4.ICMPTypeEchoReply + } + } else { + x := addr.As16() + ip = x[:] + msg = icmp.Message{ + Code: 1, + Body: &icmp.Echo{ + ID: id, + Seq: seq, + Data: b, + }, + } + if isrequest { + msg.Type = ipv6.ICMPTypeEchoRequest + } else { + msg.Type = ipv6.ICMPTypeEchoReply + } + } + buf := pbuf.NewBytes(8192) + defer buf.ManualDestroy() + var ( + data []byte + err error + n int + ) + buf.V(func(bin []byte) { + data, err = msg.Marshal(bin[:0]) + if err != nil { + return + } + _, err = conn.inner.WriteTo(data, &net.IPAddr{ + IP: ip, + Zone: addr.Zone(), + }) + if err == nil { + n = len(b) + } + }) + return n, err +} diff --git a/gold/p2p/icmp/init.go b/gold/p2p/icmp/init.go new file mode 100644 index 0000000..8e2f5fc --- /dev/null +++ b/gold/p2p/icmp/init.go @@ -0,0 +1,26 @@ +// Package icmp for non-privileged datagram-oriented ICMP endpoints, +// currently only Darwin and Linux support this. +package icmp + +import ( + "net/netip" + + "github.com/fumiama/WireGold/gold/p2p" + "github.com/fumiama/WireGold/internal/file" +) + +func NewEndpoint(endpoint string, _ ...any) (p2p.EndPoint, error) { + addr, err := netip.ParseAddr(endpoint) + if err != nil { + return nil, err + } + return (*EndPoint)(&addr), nil +} + +func init() { + name := file.FolderName() + _, hasexist := p2p.Register(name, NewEndpoint) + if hasexist { + panic("network " + name + " has been registered") + } +} diff --git a/gold/p2p/ip/ip.go b/gold/p2p/ip/ip.go index 6a3fd94..b5b75f3 100644 --- a/gold/p2p/ip/ip.go +++ b/gold/p2p/ip/ip.go @@ -20,7 +20,7 @@ func (ep *EndPoint) Network() string { return ep.addr.Network() } -func (ep *EndPoint) Euqal(ep2 p2p.EndPoint) bool { +func (ep *EndPoint) Equal(ep2 p2p.EndPoint) bool { if ep == nil || ep2 == nil { return ep == nil && ep2 == nil } @@ -64,6 +64,9 @@ func (conn *Conn) LocalAddr() p2p.EndPoint { func (conn *Conn) ReadFromPeer(b []byte) (int, p2p.EndPoint, error) { n, addr, err := conn.conn.ReadFromIP(b) + if err != nil { + return 0, nil, err + } return n, &EndPoint{ addr: addr, ptcl: conn.ep.ptcl, diff --git a/gold/p2p/tcp/tcp.go b/gold/p2p/tcp/tcp.go index 9db3aae..4469a69 100644 --- a/gold/p2p/tcp/tcp.go +++ b/gold/p2p/tcp/tcp.go @@ -32,7 +32,7 @@ func (ep *EndPoint) Network() string { return ep.addr.Network() } -func (ep *EndPoint) Euqal(ep2 p2p.EndPoint) bool { +func (ep *EndPoint) Equal(ep2 p2p.EndPoint) bool { if ep == nil || ep2 == nil { return ep == nil && ep2 == nil } diff --git a/gold/p2p/udp/udp.go b/gold/p2p/udp/udp.go index e91af9d..aeabdc6 100644 --- a/gold/p2p/udp/udp.go +++ b/gold/p2p/udp/udp.go @@ -16,7 +16,7 @@ func (ep *EndPoint) Network() string { return (*net.UDPAddr)(ep).Network() } -func (ep *EndPoint) Euqal(ep2 p2p.EndPoint) bool { +func (ep *EndPoint) Equal(ep2 p2p.EndPoint) bool { if ep == nil || ep2 == nil { return ep == nil && ep2 == nil } @@ -50,6 +50,9 @@ func (conn *Conn) LocalAddr() p2p.EndPoint { func (conn *Conn) ReadFromPeer(b []byte) (int, p2p.EndPoint, error) { n, addr, err := (*net.UDPConn)(conn).ReadFromUDP(b) + if err != nil { + return 0, nil, err + } return n, (*EndPoint)(addr), err } diff --git a/gold/p2p/udplite/udp.go b/gold/p2p/udplite/udp.go index 7905fc4..480cfd6 100644 --- a/gold/p2p/udplite/udp.go +++ b/gold/p2p/udplite/udp.go @@ -18,7 +18,7 @@ func (ep *EndPoint) Network() string { return "udplite" } -func (ep *EndPoint) Euqal(ep2 p2p.EndPoint) bool { +func (ep *EndPoint) Equal(ep2 p2p.EndPoint) bool { if ep == nil || ep2 == nil { return ep == nil && ep2 == nil } @@ -52,6 +52,9 @@ func (conn *Conn) LocalAddr() p2p.EndPoint { func (conn *Conn) ReadFromPeer(b []byte) (int, p2p.EndPoint, error) { n, addr, err := (*net.UDPConn)(conn).ReadFromUDP(b) + if err != nil { + return 0, nil, err + } return n, (*EndPoint)(addr), err } diff --git a/gold/proto/nat/nat.go b/gold/proto/nat/nat.go index 27fd805..cd42f73 100644 --- a/gold/proto/nat/nat.go +++ b/gold/proto/nat/nat.go @@ -39,7 +39,7 @@ func init() { if err == nil { p, ok := peer.Me().IsInPeer(ps) if ok { - if bin.IsNilInterface(p.EndPoint()) || !p.EndPoint().Euqal(addr) { + if bin.IsNilInterface(p.EndPoint()) || !p.EndPoint().Equal(addr) { p.SetEndPoint(addr) logrus.Infoln(file.Header(), "notify set ep of peer", ps, "to", ep) } diff --git a/upper/services/tunnel/tunnel.go b/upper/services/tunnel/tunnel.go index 0146eb8..a6f37d4 100644 --- a/upper/services/tunnel/tunnel.go +++ b/upper/services/tunnel/tunnel.go @@ -8,6 +8,7 @@ import ( "github.com/sirupsen/logrus" + _ "github.com/fumiama/WireGold/gold/p2p/icmp" // support icmp connection _ "github.com/fumiama/WireGold/gold/p2p/ip" // support ip connection _ "github.com/fumiama/WireGold/gold/p2p/tcp" // support tcp connection _ "github.com/fumiama/WireGold/gold/p2p/udp" // support udp connection diff --git a/upper/services/tunnel/tunnel_icmp_test.go b/upper/services/tunnel/tunnel_icmp_test.go new file mode 100644 index 0000000..b6fdca6 --- /dev/null +++ b/upper/services/tunnel/tunnel_icmp_test.go @@ -0,0 +1,466 @@ +//go:build linux + +package tunnel + +import ( + "bytes" + "crypto/rand" + "encoding/hex" + "fmt" + "io" + "os" + "os/exec" + "runtime" + "strconv" + "testing" + "time" + + curve "github.com/fumiama/go-x25519" + "github.com/sirupsen/logrus" + "golang.org/x/sys/unix" + + "github.com/fumiama/WireGold/gold/link" +) + +const ( + icmpNS1 = "wgtest_ns1" + icmpNS2 = "wgtest_ns2" + icmpIP1 = "10.0.0.1" + icmpIP2 = "10.0.0.2" + icmpVeth1 = "veth1" + icmpVeth2 = "veth2" +) + +// setupICMPNetns creates two network namespaces connected by a veth pair. +// It returns a cleanup function. Requires root. +func setupICMPNetns(t *testing.T) func() { + t.Helper() + + cmds := [][]string{ + {"ip", "netns", "add", icmpNS1}, + {"ip", "netns", "add", icmpNS2}, + {"ip", "link", "add", icmpVeth1, "type", "veth", "peer", "name", icmpVeth2}, + {"ip", "link", "set", icmpVeth1, "netns", icmpNS1}, + {"ip", "link", "set", icmpVeth2, "netns", icmpNS2}, + {"ip", "netns", "exec", icmpNS1, "ifconfig", icmpVeth1, icmpIP1, "up"}, + {"ip", "netns", "exec", icmpNS2, "ifconfig", icmpVeth2, icmpIP2, "up"}, + } + + for _, args := range cmds { + if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { + // best-effort cleanup + exec.Command("ip", "netns", "del", icmpNS1).Run() + exec.Command("ip", "netns", "del", icmpNS2).Run() + t.Fatalf("setup netns: %v failed: %v\n%s", args, err, out) + } + } + + return func() { + exec.Command("ip", "netns", "del", icmpNS1).Run() + exec.Command("ip", "netns", "del", icmpNS2).Run() + } +} + +// enterNetns pins the current goroutine to its OS thread, switches into +// the named network namespace, and returns a function that restores the +// original namespace and unlocks the thread. +func enterNetns(nsName string) (func(), error) { + runtime.LockOSThread() + + origFd, err := unix.Open("/proc/self/ns/net", unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + runtime.UnlockOSThread() + return nil, fmt.Errorf("open current netns: %w", err) + } + + targetFd, err := unix.Open("/var/run/netns/"+nsName, unix.O_RDONLY|unix.O_CLOEXEC, 0) + if err != nil { + unix.Close(origFd) + runtime.UnlockOSThread() + return nil, fmt.Errorf("open target netns %s: %w", nsName, err) + } + + if err := unix.Setns(targetFd, unix.CLONE_NEWNET); err != nil { + unix.Close(targetFd) + unix.Close(origFd) + runtime.UnlockOSThread() + return nil, fmt.Errorf("setns to %s: %w", nsName, err) + } + unix.Close(targetFd) + + return func() { + unix.Setns(origFd, unix.CLONE_NEWNET) + unix.Close(origFd) + runtime.UnlockOSThread() + }, nil +} + +// initMeInNetns initializes a link.Me at dst inside the given network namespace. +// The underlying socket fd remains bound to that namespace after return. +func initMeInNetns(t testing.TB, nsName string, cfg *link.MyConfig, dst *link.Me) { + t.Helper() + var merr any + done := make(chan struct{}) + go func() { + defer func() { + if r := recover(); r != nil { + merr = r + } + close(done) + }() + restore, err := enterNetns(nsName) + if err != nil { + merr = err + return + } + defer restore() + *dst = link.NewMe(cfg) + }() + <-done + if merr != nil { + t.Fatalf("initMeInNetns(%s): %v", nsName, merr) + } +} + +func TestTunnelICMP(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("skipping ICMP test: requires root") + } + for i := 1; i <= 4; i++ { + sz := 1024 * i + if !t.Run(strconv.Itoa(sz), func(t *testing.T) { + testTunnelICMP(t, uint16(sz)) + }) { + return + } + } +} + +func testTunnelICMP(t *testing.T, mtu uint16) { + logrus.SetLevel(logrus.DebugLevel) + logrus.SetFormatter(&logFormat{enableColor: false}) + + cleanup := setupICMPNetns(t) + defer cleanup() + + testICMPTunnel(t, true, false, nil, mtu) // plain text + testICMPTunnel(t, false, false, nil, mtu) // normal + + testICMPTunnel(t, true, true, nil, mtu) // plain text + base14 + testICMPTunnel(t, false, true, nil, mtu) // normal + base14 + + var buf [32]byte + if _, err := rand.Read(buf[:]); err != nil { + t.Fatal(err) + } + testICMPTunnel(t, false, false, &buf, mtu) // preshared + testICMPTunnel(t, false, true, &buf, mtu) // preshared + base14 +} + +func testICMPTunnel(t *testing.T, isplain, isbase14 bool, pshk *[32]byte, mtu uint16) { + nw := "icmp" + fmt.Println("start", nw, "testing, mtu", mtu, "plain", isplain, "b14", isbase14, "pshk", pshk != nil) + + selfpk, err := curve.New(nil) + if err != nil { + t.Fatal(err) + } + peerpk, err := curve.New(nil) + if err != nil { + t.Fatal(err) + } + t.Log("my priv key:", hex.EncodeToString(selfpk.Private()[:])) + t.Log("my publ key:", hex.EncodeToString(selfpk.Public()[:])) + t.Log("peer priv key:", hex.EncodeToString(peerpk.Private()[:])) + t.Log("peer publ key:", hex.EncodeToString(peerpk.Public()[:])) + + var m link.Me + initMeInNetns(t, icmpNS1, &link.MyConfig{ + MyIPwithMask: "192.168.1.2/32", + MyEndpoint: icmpIP1, + Network: nw, + PrivateKey: selfpk.Private(), + SrcPort: 1, + DstPort: 1, + MTU: mtu, + Base14: isbase14, + }, &m) + defer m.Close() + + var p link.Me + initMeInNetns(t, icmpNS2, &link.MyConfig{ + MyIPwithMask: "192.168.1.3/32", + MyEndpoint: icmpIP2, + Network: nw, + PrivateKey: peerpk.Private(), + SrcPort: 1, + DstPort: 1, + MTU: mtu, + Base14: isbase14, + }, &p) + defer p.Close() + + ppp := peerpk.Public() + spp := selfpk.Public() + if isplain { + ppp = nil + spp = nil + } + + m.AddPeer(&link.PeerConfig{ + PeerIP: "192.168.1.3", + EndPoint: icmpIP2, + AllowedIPs: []string{"192.168.1.3/32"}, + PubicKey: ppp, + PresharedKey: pshk, + MTU: mtu, + MTURandomRange: mtu / 2, + UseZstd: true, + DoublePacket: true, + }) + p.AddPeer(&link.PeerConfig{ + PeerIP: "192.168.1.2", + EndPoint: icmpIP1, + AllowedIPs: []string{"192.168.1.2/32"}, + PubicKey: spp, + PresharedKey: pshk, + MTU: mtu, + MTURandomRange: mtu / 2, + UseZstd: true, + }) + + tunnme, err := Create(&m, "192.168.1.3") + if err != nil { + t.Fatal(err) + } + tunnme.Start(1, 1, 4096) + tunnpeer, err := Create(&p, "192.168.1.2") + if err != nil { + t.Fatal(err) + } + tunnpeer.Start(1, 1, 4096) + + time.Sleep(time.Second) // wait link up + + sendb := ([]byte)("1234") + go tunnme.Write(sendb) + buf := make([]byte, 4) + tunnpeer.Read(buf) + if string(sendb) != string(buf) { + logrus.Errorln("error: recv", buf, "expect", sendb) + t.Fail() + } + + sendb = make([]byte, mtu+4) + for i := 0; i < len(sendb); i++ { + sendb[i] = byte(i) + } + + for i := 1; i < len(sendb); i++ { + rand.Read(sendb[:i]) + go tunnme.Write(sendb[:i]) + rbuf := make([]byte, i) + _, err = io.ReadFull(&tunnpeer, rbuf) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(sendb[:i], rbuf) { + t.Fatal("error: recv", i, "bytes data") + } + } + + for i := 0; i < len(sendb); i++ { + sendb[i] = ^byte(i) + } + tunnme.Write(sendb) + rd := bytes.NewBuffer(nil) + + tm := time.AfterFunc(time.Second*2, func() { + tunnme.Stop() + tunnpeer.Stop() + }) + defer tm.Stop() + + _, err = io.CopyBuffer(rd, &tunnpeer, make([]byte, 200)) + if err != nil { + t.Fatal(err) + } + if string(sendb) != rd.String() { + t.Fatal("error: recv fragmented data") + } +} + +func BenchmarkTunnelICMP(b *testing.B) { + if os.Getuid() != 0 { + b.Skip("skipping ICMP benchmark: requires root") + } + benchmarkTunnelNetworkICMP(b, 4096) +} + +func BenchmarkTunnelICMPSmallMTU(b *testing.B) { + if os.Getuid() != 0 { + b.Skip("skipping ICMP benchmark: requires root") + } + benchmarkTunnelNetworkICMP(b, 1024) +} + +func benchmarkTunnelNetworkICMP(b *testing.B, mtu uint16) { + logrus.SetLevel(logrus.ErrorLevel) + logrus.SetFormatter(&logFormat{enableColor: false}) + + cleanup := setupICMPBenchNetns(b) + defer cleanup() + + for i := 1; i <= 4; i++ { + sz := 1024 * i + b.Run(fmt.Sprintf("%d-plain-nob14", sz), func(b *testing.B) { + benchmarkICMPTunnel(b, sz, true, false, nil, mtu) + }) + b.Run(fmt.Sprintf("%d-normal-nob14", sz), func(b *testing.B) { + benchmarkICMPTunnel(b, sz, false, false, nil, mtu) + }) + b.Run(fmt.Sprintf("%d-plain-b14", sz), func(b *testing.B) { + benchmarkICMPTunnel(b, sz, true, true, nil, mtu) + }) + b.Run(fmt.Sprintf("%d-normal-b14", sz), func(b *testing.B) { + benchmarkICMPTunnel(b, sz, false, true, nil, mtu) + }) + var buf [32]byte + if _, err := rand.Read(buf[:]); err != nil { + b.Fatal(err) + } + b.Run(fmt.Sprintf("%d-preshared-nob14", sz), func(b *testing.B) { + benchmarkICMPTunnel(b, sz, false, false, &buf, mtu) + }) + b.Run(fmt.Sprintf("%d-preshared-b14", sz), func(b *testing.B) { + benchmarkICMPTunnel(b, sz, false, true, &buf, mtu) + }) + } +} + +func setupICMPBenchNetns(b *testing.B) func() { + b.Helper() + + cmds := [][]string{ + {"ip", "netns", "add", icmpNS1}, + {"ip", "netns", "add", icmpNS2}, + {"ip", "link", "add", icmpVeth1, "type", "veth", "peer", "name", icmpVeth2}, + {"ip", "link", "set", icmpVeth1, "netns", icmpNS1}, + {"ip", "link", "set", icmpVeth2, "netns", icmpNS2}, + {"ip", "netns", "exec", icmpNS1, "ifconfig", icmpVeth1, icmpIP1, "up"}, + {"ip", "netns", "exec", icmpNS2, "ifconfig", icmpVeth2, icmpIP2, "up"}, + } + + for _, args := range cmds { + if out, err := exec.Command(args[0], args[1:]...).CombinedOutput(); err != nil { + exec.Command("ip", "netns", "del", icmpNS1).Run() + exec.Command("ip", "netns", "del", icmpNS2).Run() + b.Fatalf("setup netns: %v failed: %v\n%s", args, err, out) + } + } + + return func() { + exec.Command("ip", "netns", "del", icmpNS1).Run() + exec.Command("ip", "netns", "del", icmpNS2).Run() + } +} + +func benchmarkICMPTunnel(b *testing.B, sz int, isplain, isbase14 bool, pshk *[32]byte, mtu uint16) { + nw := "icmp" + + selfpk, err := curve.New(nil) + if err != nil { + b.Fatal(err) + } + peerpk, err := curve.New(nil) + if err != nil { + b.Fatal(err) + } + + var m link.Me + initMeInNetns(b, icmpNS1, &link.MyConfig{ + MyIPwithMask: "192.168.1.2/32", + MyEndpoint: icmpIP1, + Network: nw, + PrivateKey: selfpk.Private(), + SrcPort: 1, + DstPort: 1, + MTU: mtu, + Base14: isbase14, + }, &m) + defer m.Close() + + var p link.Me + initMeInNetns(b, icmpNS2, &link.MyConfig{ + MyIPwithMask: "192.168.1.3/32", + MyEndpoint: icmpIP2, + Network: nw, + PrivateKey: peerpk.Private(), + SrcPort: 1, + DstPort: 1, + MTU: mtu, + Base14: isbase14, + }, &p) + defer p.Close() + + ppp := peerpk.Public() + spp := selfpk.Public() + if isplain { + ppp = nil + spp = nil + } + + m.AddPeer(&link.PeerConfig{ + PeerIP: "192.168.1.3", + EndPoint: icmpIP2, + AllowedIPs: []string{"192.168.1.3/32"}, + PubicKey: ppp, + PresharedKey: pshk, + MTU: mtu, + MTURandomRange: mtu / 2, + UseZstd: true, + DoublePacket: true, + }) + p.AddPeer(&link.PeerConfig{ + PeerIP: "192.168.1.2", + EndPoint: icmpIP1, + AllowedIPs: []string{"192.168.1.2/32"}, + PubicKey: spp, + PresharedKey: pshk, + MTU: mtu, + MTURandomRange: mtu / 2, + UseZstd: true, + }) + + tunnme, err := Create(&m, "192.168.1.3") + if err != nil { + b.Fatal(err) + } + tunnme.Start(1, 1, 4096) + tunnpeer, err := Create(&p, "192.168.1.2") + if err != nil { + b.Fatal(err) + } + tunnpeer.Start(1, 1, 4096) + + time.Sleep(time.Second) // wait link up + + b.SetBytes(int64(sz)) + b.ResetTimer() + sendb := make([]byte, sz) + for i := 0; i < b.N; i++ { + rand.Read(sendb) + go tunnme.Write(sendb) + buf := make([]byte, sz) + _, err = io.ReadFull(&tunnpeer, buf) + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() + + time.Sleep(time.Second) // wait packets all received + + tunnme.Stop() + tunnpeer.Stop() +} diff --git a/upper/services/tunnel/tunnel_test.go b/upper/services/tunnel/tunnel_test.go index 6543f18..50a770d 100644 --- a/upper/services/tunnel/tunnel_test.go +++ b/upper/services/tunnel/tunnel_test.go @@ -103,14 +103,14 @@ func testTunnel(t *testing.T, nw string, isplain, isbase14 bool, pshk *[32]byte, t.Log("peer publ key:", hex.EncodeToString(peerpk.Public()[:])) epm := "127.0.0.1" - if nw != "ip" { + if nw != "ip" && nw != "icmp" { epm += ":0" } // under macos you need to run // // sudo ifconfig lo0 alias 127.0.0.2 epp := "127.0.0.2" - if nw != "ip" { + if nw != "ip" && nw != "icmp" { epp += ":0" } @@ -238,14 +238,14 @@ func benchmarkTunnel(b *testing.B, sz int, nw string, isplain, isbase14 bool, ps } epm := "127.0.0.1" - if nw != "ip" { + if nw != "ip" && nw != "icmp" { epm += ":0" } // under macos you need to run // // sudo ifconfig lo0 alias 127.0.0.2 epp := "127.0.0.2" - if nw != "ip" { + if nw != "ip" && nw != "icmp" { epp += ":0" } diff --git a/upper/services/wg/wg.go b/upper/services/wg/wg.go index 6cd9a28..4b6f645 100644 --- a/upper/services/wg/wg.go +++ b/upper/services/wg/wg.go @@ -9,6 +9,7 @@ import ( curve "github.com/fumiama/go-x25519" "github.com/sirupsen/logrus" + _ "github.com/fumiama/WireGold/gold/p2p/icmp" // support icmp connection _ "github.com/fumiama/WireGold/gold/p2p/ip" // support ip connection _ "github.com/fumiama/WireGold/gold/p2p/tcp" // support tcp connection _ "github.com/fumiama/WireGold/gold/p2p/udp" // support udp connection