mirror of
https://github.com/tailscale/tailscale.git
synced 2025-11-29 06:21:39 +01:00
feature/relayserver,net/udprelay: add IPv6 support (#16442)
Updates tailscale/corp#27502 Updates tailscale/corp#30043 Signed-off-by: Jordan Whited <jordan@tailscale.com>
This commit is contained in:
parent
77d19604f4
commit
3a4b439c62
@ -137,7 +137,7 @@ func (e *extension) relayServerOrInit() (relayServer, error) {
|
|||||||
return nil, errors.New("TAILSCALE_USE_WIP_CODE envvar is not set")
|
return nil, errors.New("TAILSCALE_USE_WIP_CODE envvar is not set")
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
e.server, _, err = udprelay.NewServer(e.logf, *e.port, nil)
|
e.server, err = udprelay.NewServer(e.logf, *e.port, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -57,7 +57,10 @@ type Server struct {
|
|||||||
bindLifetime time.Duration
|
bindLifetime time.Duration
|
||||||
steadyStateLifetime time.Duration
|
steadyStateLifetime time.Duration
|
||||||
bus *eventbus.Bus
|
bus *eventbus.Bus
|
||||||
uc *net.UDPConn
|
uc4 *net.UDPConn // always non-nil
|
||||||
|
uc4Port uint16 // always nonzero
|
||||||
|
uc6 *net.UDPConn // may be nil if IPv6 bind fails during initialization
|
||||||
|
uc6Port uint16 // may be zero if IPv6 bind fails during initialization
|
||||||
closeOnce sync.Once
|
closeOnce sync.Once
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
closeCh chan struct{}
|
closeCh chan struct{}
|
||||||
@ -278,13 +281,11 @@ func (e *serverEndpoint) isBound() bool {
|
|||||||
e.boundAddrPorts[1].IsValid()
|
e.boundAddrPorts[1].IsValid()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewServer constructs a [Server] listening on 0.0.0.0:'port'. IPv6 is not yet
|
// NewServer constructs a [Server] listening on port. If port is zero, then
|
||||||
// supported. Port may be 0, and what ultimately gets bound is returned as
|
// port selection is left up to the host networking stack. If
|
||||||
// 'boundPort'. If len(overrideAddrs) > 0 these will be used in place of dynamic
|
// len(overrideAddrs) > 0 these will be used in place of dynamic discovery,
|
||||||
// discovery, which is useful to override in tests.
|
// which is useful to override in tests.
|
||||||
//
|
func NewServer(logf logger.Logf, port int, overrideAddrs []netip.Addr) (s *Server, err error) {
|
||||||
// TODO: IPv6 support
|
|
||||||
func NewServer(logf logger.Logf, port int, overrideAddrs []netip.Addr) (s *Server, boundPort uint16, err error) {
|
|
||||||
s = &Server{
|
s = &Server{
|
||||||
logf: logger.WithPrefix(logf, "relayserver"),
|
logf: logger.WithPrefix(logf, "relayserver"),
|
||||||
disco: key.NewDisco(),
|
disco: key.NewDisco(),
|
||||||
@ -306,30 +307,36 @@ func NewServer(logf logger.Logf, port int, overrideAddrs []netip.Addr) (s *Serve
|
|||||||
s.bus = bus
|
s.bus = bus
|
||||||
netMon, err := netmon.New(s.bus, logf)
|
netMon, err := netmon.New(s.bus, logf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
s.netChecker = &netcheck.Client{
|
s.netChecker = &netcheck.Client{
|
||||||
NetMon: netMon,
|
NetMon: netMon,
|
||||||
Logf: logger.WithPrefix(logf, "relayserver: netcheck:"),
|
Logf: logger.WithPrefix(logf, "relayserver: netcheck:"),
|
||||||
SendPacket: func(b []byte, addrPort netip.AddrPort) (int, error) {
|
SendPacket: func(b []byte, addrPort netip.AddrPort) (int, error) {
|
||||||
return s.uc.WriteToUDPAddrPort(b, addrPort)
|
if addrPort.Addr().Is4() {
|
||||||
|
return s.uc4.WriteToUDPAddrPort(b, addrPort)
|
||||||
|
} else if s.uc6 != nil {
|
||||||
|
return s.uc6.WriteToUDPAddrPort(b, addrPort)
|
||||||
|
} else {
|
||||||
|
return 0, errors.New("IPv6 socket is not bound")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
boundPort, err = s.listenOn(port)
|
err = s.listenOn(port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.wg.Add(1)
|
|
||||||
go s.packetReadLoop()
|
|
||||||
s.wg.Add(1)
|
|
||||||
go s.endpointGCLoop()
|
|
||||||
if len(overrideAddrs) > 0 {
|
if len(overrideAddrs) > 0 {
|
||||||
addrPorts := make(set.Set[netip.AddrPort], len(overrideAddrs))
|
addrPorts := make(set.Set[netip.AddrPort], len(overrideAddrs))
|
||||||
for _, addr := range overrideAddrs {
|
for _, addr := range overrideAddrs {
|
||||||
if addr.IsValid() {
|
if addr.IsValid() {
|
||||||
addrPorts.Add(netip.AddrPortFrom(addr, boundPort))
|
if addr.Is4() {
|
||||||
|
addrPorts.Add(netip.AddrPortFrom(addr, s.uc4Port))
|
||||||
|
} else if s.uc6 != nil {
|
||||||
|
addrPorts.Add(netip.AddrPortFrom(addr, s.uc6Port))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.addrPorts = addrPorts.Slice()
|
s.addrPorts = addrPorts.Slice()
|
||||||
@ -337,7 +344,17 @@ func NewServer(logf logger.Logf, port int, overrideAddrs []netip.Addr) (s *Serve
|
|||||||
s.wg.Add(1)
|
s.wg.Add(1)
|
||||||
go s.addrDiscoveryLoop()
|
go s.addrDiscoveryLoop()
|
||||||
}
|
}
|
||||||
return s, boundPort, nil
|
|
||||||
|
s.wg.Add(1)
|
||||||
|
go s.packetReadLoop(s.uc4)
|
||||||
|
if s.uc6 != nil {
|
||||||
|
s.wg.Add(1)
|
||||||
|
go s.packetReadLoop(s.uc6)
|
||||||
|
}
|
||||||
|
s.wg.Add(1)
|
||||||
|
go s.endpointGCLoop()
|
||||||
|
|
||||||
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) addrDiscoveryLoop() {
|
func (s *Server) addrDiscoveryLoop() {
|
||||||
@ -351,14 +368,17 @@ func (s *Server) addrDiscoveryLoop() {
|
|||||||
addrPorts.Make()
|
addrPorts.Make()
|
||||||
|
|
||||||
// get local addresses
|
// get local addresses
|
||||||
localPort := s.uc.LocalAddr().(*net.UDPAddr).Port
|
|
||||||
ips, _, err := netmon.LocalAddresses()
|
ips, _, err := netmon.LocalAddresses()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
if ip.IsValid() {
|
if ip.IsValid() {
|
||||||
addrPorts.Add(netip.AddrPortFrom(ip, uint16(localPort)))
|
if ip.Is4() {
|
||||||
|
addrPorts.Add(netip.AddrPortFrom(ip, s.uc4Port))
|
||||||
|
} else {
|
||||||
|
addrPorts.Add(netip.AddrPortFrom(ip, s.uc6Port))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,24 +433,52 @@ func (s *Server) addrDiscoveryLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) listenOn(port int) (uint16, error) {
|
// listenOn binds an IPv4 and IPv6 socket to port. We consider it successful if
|
||||||
uc, err := net.ListenUDP("udp4", &net.UDPAddr{Port: port})
|
// we manage to bind the IPv4 socket.
|
||||||
if err != nil {
|
//
|
||||||
return 0, err
|
// The requested port may be zero, in which case port selection is left up to
|
||||||
|
// the host networking stack. We make no attempt to bind a consistent port
|
||||||
|
// across IPv4 and IPv6 if the requested port is zero.
|
||||||
|
//
|
||||||
|
// TODO: make these "re-bindable" in similar fashion to magicsock as a means to
|
||||||
|
// deal with EDR software closing them. http://go/corp/30118
|
||||||
|
func (s *Server) listenOn(port int) error {
|
||||||
|
for _, network := range []string{"udp4", "udp6"} {
|
||||||
|
uc, err := net.ListenUDP(network, &net.UDPAddr{Port: port})
|
||||||
|
if err != nil {
|
||||||
|
if network == "udp4" {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
s.logf("ignoring IPv6 bind failure: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: set IP_PKTINFO sockopt
|
||||||
|
_, boundPortStr, err := net.SplitHostPort(uc.LocalAddr().String())
|
||||||
|
if err != nil {
|
||||||
|
uc.Close()
|
||||||
|
if s.uc4 != nil {
|
||||||
|
s.uc4.Close()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
portUint, err := strconv.ParseUint(boundPortStr, 10, 16)
|
||||||
|
if err != nil {
|
||||||
|
uc.Close()
|
||||||
|
if s.uc4 != nil {
|
||||||
|
s.uc4.Close()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if network == "udp4" {
|
||||||
|
s.uc4 = uc
|
||||||
|
s.uc4Port = uint16(portUint)
|
||||||
|
} else {
|
||||||
|
s.uc6 = uc
|
||||||
|
s.uc6Port = uint16(portUint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// TODO: set IP_PKTINFO sockopt
|
return nil
|
||||||
_, boundPortStr, err := net.SplitHostPort(uc.LocalAddr().String())
|
|
||||||
if err != nil {
|
|
||||||
s.uc.Close()
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
boundPort, err := strconv.ParseUint(boundPortStr, 10, 16)
|
|
||||||
if err != nil {
|
|
||||||
s.uc.Close()
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
s.uc = uc
|
|
||||||
return uint16(boundPort), nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close closes the server.
|
// Close closes the server.
|
||||||
@ -438,7 +486,10 @@ func (s *Server) Close() error {
|
|||||||
s.closeOnce.Do(func() {
|
s.closeOnce.Do(func() {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
s.uc.Close()
|
s.uc4.Close()
|
||||||
|
if s.uc6 != nil {
|
||||||
|
s.uc6.Close()
|
||||||
|
}
|
||||||
close(s.closeCh)
|
close(s.closeCh)
|
||||||
s.wg.Wait()
|
s.wg.Wait()
|
||||||
clear(s.byVNI)
|
clear(s.byVNI)
|
||||||
@ -507,7 +558,7 @@ func (s *Server) handlePacket(from netip.AddrPort, b []byte, uw udpWriter) {
|
|||||||
e.handlePacket(from, gh, b, uw, s.discoPublic)
|
e.handlePacket(from, gh, b, uw, s.discoPublic)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) packetReadLoop() {
|
func (s *Server) packetReadLoop(uc *net.UDPConn) {
|
||||||
defer func() {
|
defer func() {
|
||||||
s.wg.Done()
|
s.wg.Done()
|
||||||
s.Close()
|
s.Close()
|
||||||
@ -515,11 +566,11 @@ func (s *Server) packetReadLoop() {
|
|||||||
b := make([]byte, 1<<16-1)
|
b := make([]byte, 1<<16-1)
|
||||||
for {
|
for {
|
||||||
// TODO: extract laddr from IP_PKTINFO for use in reply
|
// TODO: extract laddr from IP_PKTINFO for use in reply
|
||||||
n, from, err := s.uc.ReadFromUDPAddrPort(b)
|
n, from, err := uc.ReadFromUDPAddrPort(b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.handlePacket(from, b[:n], s.uc)
|
s.handlePacket(from, b[:n], uc)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,7 +29,7 @@ type testClient struct {
|
|||||||
|
|
||||||
func newTestClient(t *testing.T, vni uint32, serverEndpoint netip.AddrPort, local key.DiscoPrivate, remote, server key.DiscoPublic) *testClient {
|
func newTestClient(t *testing.T, vni uint32, serverEndpoint netip.AddrPort, local key.DiscoPrivate, remote, server key.DiscoPublic) *testClient {
|
||||||
rAddr := &net.UDPAddr{IP: serverEndpoint.Addr().AsSlice(), Port: int(serverEndpoint.Port())}
|
rAddr := &net.UDPAddr{IP: serverEndpoint.Addr().AsSlice(), Port: int(serverEndpoint.Port())}
|
||||||
uc, err := net.DialUDP("udp4", nil, rAddr)
|
uc, err := net.DialUDP("udp", nil, rAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@ -180,85 +180,101 @@ func TestServer(t *testing.T) {
|
|||||||
discoA := key.NewDisco()
|
discoA := key.NewDisco()
|
||||||
discoB := key.NewDisco()
|
discoB := key.NewDisco()
|
||||||
|
|
||||||
ipv4LoopbackAddr := netip.MustParseAddr("127.0.0.1")
|
cases := []struct {
|
||||||
|
name string
|
||||||
server, _, err := NewServer(t.Logf, 0, []netip.Addr{ipv4LoopbackAddr})
|
overrideAddrs []netip.Addr
|
||||||
if err != nil {
|
}{
|
||||||
t.Fatal(err)
|
{
|
||||||
}
|
name: "over ipv4",
|
||||||
defer server.Close()
|
overrideAddrs: []netip.Addr{netip.MustParseAddr("127.0.0.1")},
|
||||||
|
},
|
||||||
endpoint, err := server.AllocateEndpoint(discoA.Public(), discoB.Public())
|
{
|
||||||
if err != nil {
|
name: "over ipv6",
|
||||||
t.Fatal(err)
|
overrideAddrs: []netip.Addr{netip.MustParseAddr("::1")},
|
||||||
}
|
},
|
||||||
dupEndpoint, err := server.AllocateEndpoint(discoA.Public(), discoB.Public())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// We expect the same endpoint details pre-handshake.
|
for _, tt := range cases {
|
||||||
if diff := cmp.Diff(dupEndpoint, endpoint, cmpopts.EquateComparable(netip.AddrPort{}, key.DiscoPublic{})); diff != "" {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Fatalf("wrong dupEndpoint (-got +want)\n%s", diff)
|
server, err := NewServer(t.Logf, 0, tt.overrideAddrs)
|
||||||
}
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
if len(endpoint.AddrPorts) != 1 {
|
endpoint, err := server.AllocateEndpoint(discoA.Public(), discoB.Public())
|
||||||
t.Fatalf("unexpected endpoint.AddrPorts: %v", endpoint.AddrPorts)
|
if err != nil {
|
||||||
}
|
t.Fatal(err)
|
||||||
tcA := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoA, discoB.Public(), endpoint.ServerDisco)
|
}
|
||||||
defer tcA.close()
|
dupEndpoint, err := server.AllocateEndpoint(discoA.Public(), discoB.Public())
|
||||||
tcB := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoB, discoA.Public(), endpoint.ServerDisco)
|
if err != nil {
|
||||||
defer tcB.close()
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
tcA.handshake(t)
|
// We expect the same endpoint details pre-handshake.
|
||||||
tcB.handshake(t)
|
if diff := cmp.Diff(dupEndpoint, endpoint, cmpopts.EquateComparable(netip.AddrPort{}, key.DiscoPublic{})); diff != "" {
|
||||||
|
t.Fatalf("wrong dupEndpoint (-got +want)\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
dupEndpoint, err = server.AllocateEndpoint(discoA.Public(), discoB.Public())
|
if len(endpoint.AddrPorts) != 1 {
|
||||||
if err != nil {
|
t.Fatalf("unexpected endpoint.AddrPorts: %v", endpoint.AddrPorts)
|
||||||
t.Fatal(err)
|
}
|
||||||
}
|
tcA := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoA, discoB.Public(), endpoint.ServerDisco)
|
||||||
// We expect the same endpoint details post-handshake.
|
defer tcA.close()
|
||||||
if diff := cmp.Diff(dupEndpoint, endpoint, cmpopts.EquateComparable(netip.AddrPort{}, key.DiscoPublic{})); diff != "" {
|
tcB := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoB, discoA.Public(), endpoint.ServerDisco)
|
||||||
t.Fatalf("wrong dupEndpoint (-got +want)\n%s", diff)
|
defer tcB.close()
|
||||||
}
|
|
||||||
|
|
||||||
txToB := []byte{1, 2, 3}
|
tcA.handshake(t)
|
||||||
tcA.writeDataPkt(t, txToB)
|
tcB.handshake(t)
|
||||||
rxFromA := tcB.readDataPkt(t)
|
|
||||||
if !bytes.Equal(txToB, rxFromA) {
|
|
||||||
t.Fatal("unexpected msg A->B")
|
|
||||||
}
|
|
||||||
|
|
||||||
txToA := []byte{4, 5, 6}
|
dupEndpoint, err = server.AllocateEndpoint(discoA.Public(), discoB.Public())
|
||||||
tcB.writeDataPkt(t, txToA)
|
if err != nil {
|
||||||
rxFromB := tcA.readDataPkt(t)
|
t.Fatal(err)
|
||||||
if !bytes.Equal(txToA, rxFromB) {
|
}
|
||||||
t.Fatal("unexpected msg B->A")
|
// We expect the same endpoint details post-handshake.
|
||||||
}
|
if diff := cmp.Diff(dupEndpoint, endpoint, cmpopts.EquateComparable(netip.AddrPort{}, key.DiscoPublic{})); diff != "" {
|
||||||
|
t.Fatalf("wrong dupEndpoint (-got +want)\n%s", diff)
|
||||||
|
}
|
||||||
|
|
||||||
tcAOnNewPort := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoA, discoB.Public(), endpoint.ServerDisco)
|
txToB := []byte{1, 2, 3}
|
||||||
tcAOnNewPort.handshakeGeneration = tcA.handshakeGeneration + 1
|
tcA.writeDataPkt(t, txToB)
|
||||||
defer tcAOnNewPort.close()
|
rxFromA := tcB.readDataPkt(t)
|
||||||
|
if !bytes.Equal(txToB, rxFromA) {
|
||||||
|
t.Fatal("unexpected msg A->B")
|
||||||
|
}
|
||||||
|
|
||||||
// Handshake client A on a new source IP:port, verify we receive packets on the new binding
|
txToA := []byte{4, 5, 6}
|
||||||
tcAOnNewPort.handshake(t)
|
tcB.writeDataPkt(t, txToA)
|
||||||
txToAOnNewPort := []byte{7, 8, 9}
|
rxFromB := tcA.readDataPkt(t)
|
||||||
tcB.writeDataPkt(t, txToAOnNewPort)
|
if !bytes.Equal(txToA, rxFromB) {
|
||||||
rxFromB = tcAOnNewPort.readDataPkt(t)
|
t.Fatal("unexpected msg B->A")
|
||||||
if !bytes.Equal(txToAOnNewPort, rxFromB) {
|
}
|
||||||
t.Fatal("unexpected msg B->A")
|
|
||||||
}
|
|
||||||
|
|
||||||
tcBOnNewPort := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoB, discoA.Public(), endpoint.ServerDisco)
|
tcAOnNewPort := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoA, discoB.Public(), endpoint.ServerDisco)
|
||||||
tcBOnNewPort.handshakeGeneration = tcB.handshakeGeneration + 1
|
tcAOnNewPort.handshakeGeneration = tcA.handshakeGeneration + 1
|
||||||
defer tcBOnNewPort.close()
|
defer tcAOnNewPort.close()
|
||||||
|
|
||||||
// Handshake client B on a new source IP:port, verify we receive packets on the new binding
|
// Handshake client A on a new source IP:port, verify we receive packets on the new binding
|
||||||
tcBOnNewPort.handshake(t)
|
tcAOnNewPort.handshake(t)
|
||||||
txToBOnNewPort := []byte{7, 8, 9}
|
txToAOnNewPort := []byte{7, 8, 9}
|
||||||
tcAOnNewPort.writeDataPkt(t, txToBOnNewPort)
|
tcB.writeDataPkt(t, txToAOnNewPort)
|
||||||
rxFromA = tcBOnNewPort.readDataPkt(t)
|
rxFromB = tcAOnNewPort.readDataPkt(t)
|
||||||
if !bytes.Equal(txToBOnNewPort, rxFromA) {
|
if !bytes.Equal(txToAOnNewPort, rxFromB) {
|
||||||
t.Fatal("unexpected msg A->B")
|
t.Fatal("unexpected msg B->A")
|
||||||
|
}
|
||||||
|
|
||||||
|
tcBOnNewPort := newTestClient(t, endpoint.VNI, endpoint.AddrPorts[0], discoB, discoA.Public(), endpoint.ServerDisco)
|
||||||
|
tcBOnNewPort.handshakeGeneration = tcB.handshakeGeneration + 1
|
||||||
|
defer tcBOnNewPort.close()
|
||||||
|
|
||||||
|
// Handshake client B on a new source IP:port, verify we receive packets on the new binding
|
||||||
|
tcBOnNewPort.handshake(t)
|
||||||
|
txToBOnNewPort := []byte{7, 8, 9}
|
||||||
|
tcAOnNewPort.writeDataPkt(t, txToBOnNewPort)
|
||||||
|
rxFromA = tcBOnNewPort.readDataPkt(t)
|
||||||
|
if !bytes.Equal(txToBOnNewPort, rxFromA) {
|
||||||
|
t.Fatal("unexpected msg A->B")
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user