mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-10-31 16:22:03 +01:00 
			
		
		
		
	perl -i -npe 's,netaddr.IPPrefixFrom,netip.PrefixFrom,' $(git grep -l -F netaddr.)
    perl -i -npe 's,netaddr.IPPortFrom,netip.AddrPortFrom,' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IPPrefix,netip.Prefix,g' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IPPort,netip.AddrPort,g' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IP\b,netip.Addr,g' $(git grep -l -F netaddr. )
    perl -i -npe 's,netaddr.IPv6Raw\b,netip.AddrFrom16,g' $(git grep -l -F netaddr. )
    goimports -w .
Then delete some stuff from the net/netaddr shim package which is no
longer neeed.
Updates #5162
Change-Id: Ia7a86893fe21c7e3ee1ec823e8aba288d4566cd8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
		
	
			
		
			
				
	
	
		
			511 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			511 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
 | |
| // Use of this source code is governed by a BSD-style
 | |
| // license that can be found in the LICENSE file.
 | |
| 
 | |
| package natlab
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"net"
 | |
| 	"net/netip"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"tailscale.com/tstest"
 | |
| )
 | |
| 
 | |
| func TestAllocIPs(t *testing.T) {
 | |
| 	n := NewInternet()
 | |
| 	saw := map[netip.Addr]bool{}
 | |
| 	for i := 0; i < 255; i++ {
 | |
| 		for _, f := range []func(*Interface) netip.Addr{n.allocIPv4, n.allocIPv6} {
 | |
| 			ip := f(nil)
 | |
| 			if saw[ip] {
 | |
| 				t.Fatalf("got duplicate %v", ip)
 | |
| 			}
 | |
| 			saw[ip] = true
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// This should work:
 | |
| 	n.allocIPv6(nil)
 | |
| 
 | |
| 	// But allocating another IPv4 should panic, exhausting the
 | |
| 	// limited /24 range:
 | |
| 	defer func() {
 | |
| 		if e := recover(); fmt.Sprint(e) != "pool exhausted" {
 | |
| 			t.Errorf("unexpected panic: %v", e)
 | |
| 		}
 | |
| 	}()
 | |
| 	n.allocIPv4(nil)
 | |
| 	t.Fatalf("expected panic from IPv4")
 | |
| }
 | |
| 
 | |
| func TestSendPacket(t *testing.T) {
 | |
| 	internet := NewInternet()
 | |
| 
 | |
| 	foo := &Machine{Name: "foo"}
 | |
| 	bar := &Machine{Name: "bar"}
 | |
| 	ifFoo := foo.Attach("eth0", internet)
 | |
| 	ifBar := bar.Attach("enp0s1", internet)
 | |
| 
 | |
| 	fooAddr := netip.AddrPortFrom(ifFoo.V4(), 123)
 | |
| 	barAddr := netip.AddrPortFrom(ifBar.V4(), 456)
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	fooPC, err := foo.ListenPacket(ctx, "udp4", fooAddr.String())
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	barPC, err := bar.ListenPacket(ctx, "udp4", barAddr.String())
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	const msg = "some message"
 | |
| 	if _, err := fooPC.WriteTo([]byte(msg), net.UDPAddrFromAddrPort(barAddr)); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	buf := make([]byte, 1500) // TODO: care about MTUs in the natlab package somewhere
 | |
| 	n, addr, err := barPC.ReadFrom(buf)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	buf = buf[:n]
 | |
| 	if string(buf) != msg {
 | |
| 		t.Errorf("read %q; want %q", buf, msg)
 | |
| 	}
 | |
| 	if addr.String() != fooAddr.String() {
 | |
| 		t.Errorf("addr = %q; want %q", addr, fooAddr)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestMultiNetwork(t *testing.T) {
 | |
| 	lan := &Network{
 | |
| 		Name:    "lan",
 | |
| 		Prefix4: mustPrefix("192.168.0.0/24"),
 | |
| 	}
 | |
| 	internet := NewInternet()
 | |
| 
 | |
| 	client := &Machine{Name: "client"}
 | |
| 	nat := &Machine{Name: "nat"}
 | |
| 	server := &Machine{Name: "server"}
 | |
| 
 | |
| 	ifClient := client.Attach("eth0", lan)
 | |
| 	ifNATWAN := nat.Attach("ethwan", internet)
 | |
| 	ifNATLAN := nat.Attach("ethlan", lan)
 | |
| 	ifServer := server.Attach("eth0", internet)
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	clientPC, err := client.ListenPacket(ctx, "udp", ":123")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	natPC, err := nat.ListenPacket(ctx, "udp", ":456")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	serverPC, err := server.ListenPacket(ctx, "udp", ":789")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	clientAddr := netip.AddrPortFrom(ifClient.V4(), 123)
 | |
| 	natLANAddr := netip.AddrPortFrom(ifNATLAN.V4(), 456)
 | |
| 	natWANAddr := netip.AddrPortFrom(ifNATWAN.V4(), 456)
 | |
| 	serverAddr := netip.AddrPortFrom(ifServer.V4(), 789)
 | |
| 
 | |
| 	const msg1, msg2 = "hello", "world"
 | |
| 	if _, err := natPC.WriteTo([]byte(msg1), net.UDPAddrFromAddrPort(clientAddr)); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if _, err := natPC.WriteTo([]byte(msg2), net.UDPAddrFromAddrPort(serverAddr)); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	buf := make([]byte, 1500)
 | |
| 	n, addr, err := clientPC.ReadFrom(buf)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if string(buf[:n]) != msg1 {
 | |
| 		t.Errorf("read %q; want %q", buf[:n], msg1)
 | |
| 	}
 | |
| 	if addr.String() != natLANAddr.String() {
 | |
| 		t.Errorf("addr = %q; want %q", addr, natLANAddr)
 | |
| 	}
 | |
| 
 | |
| 	n, addr, err = serverPC.ReadFrom(buf)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	if string(buf[:n]) != msg2 {
 | |
| 		t.Errorf("read %q; want %q", buf[:n], msg2)
 | |
| 	}
 | |
| 	if addr.String() != natWANAddr.String() {
 | |
| 		t.Errorf("addr = %q; want %q", addr, natLANAddr)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| type trivialNAT struct {
 | |
| 	clientIP     netip.Addr
 | |
| 	lanIf, wanIf *Interface
 | |
| }
 | |
| 
 | |
| func (n *trivialNAT) HandleIn(p *Packet, iface *Interface) *Packet {
 | |
| 	if iface == n.wanIf && p.Dst.Addr() == n.wanIf.V4() {
 | |
| 		p.Dst = netip.AddrPortFrom(n.clientIP, p.Dst.Port())
 | |
| 	}
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| func (n trivialNAT) HandleOut(p *Packet, iface *Interface) *Packet {
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| func (n *trivialNAT) HandleForward(p *Packet, iif, oif *Interface) *Packet {
 | |
| 	// Outbound from LAN -> apply NAT, continue
 | |
| 	if iif == n.lanIf && oif == n.wanIf {
 | |
| 		if p.Src.Addr() == n.clientIP {
 | |
| 			p.Src = netip.AddrPortFrom(n.wanIf.V4(), p.Src.Port())
 | |
| 		}
 | |
| 		return p
 | |
| 	}
 | |
| 	// Return traffic to LAN, allow if right dst.
 | |
| 	if iif == n.wanIf && oif == n.lanIf && p.Dst.Addr() == n.clientIP {
 | |
| 		return p
 | |
| 	}
 | |
| 	// Else drop.
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func TestPacketHandler(t *testing.T) {
 | |
| 	lan := &Network{
 | |
| 		Name:    "lan",
 | |
| 		Prefix4: mustPrefix("192.168.0.0/24"),
 | |
| 		Prefix6: mustPrefix("fd00:916::/64"),
 | |
| 	}
 | |
| 	internet := NewInternet()
 | |
| 
 | |
| 	client := &Machine{Name: "client"}
 | |
| 	nat := &Machine{Name: "nat"}
 | |
| 	server := &Machine{Name: "server"}
 | |
| 
 | |
| 	ifClient := client.Attach("eth0", lan)
 | |
| 	ifNATWAN := nat.Attach("wan", internet)
 | |
| 	ifNATLAN := nat.Attach("lan", lan)
 | |
| 	ifServer := server.Attach("server", internet)
 | |
| 
 | |
| 	lan.SetDefaultGateway(ifNATLAN)
 | |
| 
 | |
| 	nat.PacketHandler = &trivialNAT{
 | |
| 		clientIP: ifClient.V4(),
 | |
| 		lanIf:    ifNATLAN,
 | |
| 		wanIf:    ifNATWAN,
 | |
| 	}
 | |
| 
 | |
| 	ctx := context.Background()
 | |
| 	clientPC, err := client.ListenPacket(ctx, "udp4", ":123")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	serverPC, err := server.ListenPacket(ctx, "udp4", ":456")
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	const msg = "some message"
 | |
| 	serverAddr := netip.AddrPortFrom(ifServer.V4(), 456)
 | |
| 	if _, err := clientPC.WriteTo([]byte(msg), net.UDPAddrFromAddrPort(serverAddr)); err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	buf := make([]byte, 1500) // TODO: care about MTUs in the natlab package somewhere
 | |
| 	n, addr, err := serverPC.ReadFrom(buf)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 	buf = buf[:n]
 | |
| 	if string(buf) != msg {
 | |
| 		t.Errorf("read %q; want %q", buf, msg)
 | |
| 	}
 | |
| 	mappedAddr := netip.AddrPortFrom(ifNATWAN.V4(), 123)
 | |
| 	if addr.String() != mappedAddr.String() {
 | |
| 		t.Errorf("addr = %q; want %q", addr, mappedAddr)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestFirewall(t *testing.T) {
 | |
| 	wan := NewInternet()
 | |
| 	lan := &Network{
 | |
| 		Name:    "lan",
 | |
| 		Prefix4: mustPrefix("10.0.0.0/8"),
 | |
| 	}
 | |
| 	m := &Machine{Name: "test"}
 | |
| 	trust := m.Attach("trust", lan)
 | |
| 	untrust := m.Attach("untrust", wan)
 | |
| 
 | |
| 	client := ipp("192.168.0.2:1234")
 | |
| 	serverA := ipp("2.2.2.2:5678")
 | |
| 	serverB1 := ipp("7.7.7.7:9012")
 | |
| 	serverB2 := ipp("7.7.7.7:3456")
 | |
| 
 | |
| 	t.Run("ip_port_dependent", func(t *testing.T) {
 | |
| 		f := &Firewall{
 | |
| 			TrustedInterface: trust,
 | |
| 			SessionTimeout:   30 * time.Second,
 | |
| 			Type:             AddressAndPortDependentFirewall,
 | |
| 		}
 | |
| 		testFirewall(t, f, []fwTest{
 | |
| 			// client -> A authorizes A -> client
 | |
| 			{trust, untrust, client, serverA, true},
 | |
| 			{untrust, trust, serverA, client, true},
 | |
| 			{untrust, trust, serverA, client, true},
 | |
| 
 | |
| 			// B1 -> client fails until client -> B1
 | |
| 			{untrust, trust, serverB1, client, false},
 | |
| 			{trust, untrust, client, serverB1, true},
 | |
| 			{untrust, trust, serverB1, client, true},
 | |
| 
 | |
| 			// B2 -> client still fails
 | |
| 			{untrust, trust, serverB2, client, false},
 | |
| 		})
 | |
| 	})
 | |
| 	t.Run("ip_dependent", func(t *testing.T) {
 | |
| 		f := &Firewall{
 | |
| 			TrustedInterface: trust,
 | |
| 			SessionTimeout:   30 * time.Second,
 | |
| 			Type:             AddressDependentFirewall,
 | |
| 		}
 | |
| 		testFirewall(t, f, []fwTest{
 | |
| 			// client -> A authorizes A -> client
 | |
| 			{trust, untrust, client, serverA, true},
 | |
| 			{untrust, trust, serverA, client, true},
 | |
| 			{untrust, trust, serverA, client, true},
 | |
| 
 | |
| 			// B1 -> client fails until client -> B1
 | |
| 			{untrust, trust, serverB1, client, false},
 | |
| 			{trust, untrust, client, serverB1, true},
 | |
| 			{untrust, trust, serverB1, client, true},
 | |
| 
 | |
| 			// B2 -> client also works now
 | |
| 			{untrust, trust, serverB2, client, true},
 | |
| 		})
 | |
| 	})
 | |
| 	t.Run("endpoint_independent", func(t *testing.T) {
 | |
| 		f := &Firewall{
 | |
| 			TrustedInterface: trust,
 | |
| 			SessionTimeout:   30 * time.Second,
 | |
| 			Type:             EndpointIndependentFirewall,
 | |
| 		}
 | |
| 		testFirewall(t, f, []fwTest{
 | |
| 			// client -> A authorizes A -> client
 | |
| 			{trust, untrust, client, serverA, true},
 | |
| 			{untrust, trust, serverA, client, true},
 | |
| 			{untrust, trust, serverA, client, true},
 | |
| 
 | |
| 			// B1 -> client also works
 | |
| 			{untrust, trust, serverB1, client, true},
 | |
| 
 | |
| 			// B2 -> client also works
 | |
| 			{untrust, trust, serverB2, client, true},
 | |
| 		})
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type fwTest struct {
 | |
| 	iif, oif *Interface
 | |
| 	src, dst netip.AddrPort
 | |
| 	ok       bool
 | |
| }
 | |
| 
 | |
| func testFirewall(t *testing.T, f *Firewall, tests []fwTest) {
 | |
| 	t.Helper()
 | |
| 	clock := &tstest.Clock{}
 | |
| 	f.TimeNow = clock.Now
 | |
| 	for _, test := range tests {
 | |
| 		clock.Advance(time.Second)
 | |
| 		p := &Packet{
 | |
| 			Src:     test.src,
 | |
| 			Dst:     test.dst,
 | |
| 			Payload: []byte{},
 | |
| 		}
 | |
| 		got := f.HandleForward(p, test.iif, test.oif)
 | |
| 		gotOK := got != nil
 | |
| 		if gotOK != test.ok {
 | |
| 			t.Errorf("iif=%s oif=%s src=%s dst=%s got ok=%v, want ok=%v", test.iif, test.oif, test.src, test.dst, gotOK, test.ok)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func ipp(str string) netip.AddrPort {
 | |
| 	ipp, err := netip.ParseAddrPort(str)
 | |
| 	if err != nil {
 | |
| 		panic(err)
 | |
| 	}
 | |
| 	return ipp
 | |
| }
 | |
| 
 | |
| func TestNAT(t *testing.T) {
 | |
| 	internet := NewInternet()
 | |
| 	lan := &Network{
 | |
| 		Name:    "LAN",
 | |
| 		Prefix4: mustPrefix("192.168.0.0/24"),
 | |
| 	}
 | |
| 	m := &Machine{Name: "NAT"}
 | |
| 	wanIf := m.Attach("wan", internet)
 | |
| 	lanIf := m.Attach("lan", lan)
 | |
| 
 | |
| 	t.Run("endpoint_independent_mapping", func(t *testing.T) {
 | |
| 		n := &SNAT44{
 | |
| 			Machine:           m,
 | |
| 			ExternalInterface: wanIf,
 | |
| 			Type:              EndpointIndependentNAT,
 | |
| 			Firewall: &Firewall{
 | |
| 				TrustedInterface: lanIf,
 | |
| 			},
 | |
| 		}
 | |
| 		testNAT(t, n, lanIf, wanIf, []natTest{
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("2.2.2.2:5678"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("7.7.7.7:9012"),
 | |
| 				wantNewMapping: false,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:2345"),
 | |
| 				dst:            ipp("7.7.7.7:9012"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 		})
 | |
| 	})
 | |
| 
 | |
| 	t.Run("address_dependent_mapping", func(t *testing.T) {
 | |
| 		n := &SNAT44{
 | |
| 			Machine:           m,
 | |
| 			ExternalInterface: wanIf,
 | |
| 			Type:              AddressDependentNAT,
 | |
| 			Firewall: &Firewall{
 | |
| 				TrustedInterface: lanIf,
 | |
| 			},
 | |
| 		}
 | |
| 		testNAT(t, n, lanIf, wanIf, []natTest{
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("2.2.2.2:5678"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("2.2.2.2:9012"),
 | |
| 				wantNewMapping: false,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("7.7.7.7:9012"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("7.7.7.7:1234"),
 | |
| 				wantNewMapping: false,
 | |
| 			},
 | |
| 		})
 | |
| 	})
 | |
| 
 | |
| 	t.Run("address_and_port_dependent_mapping", func(t *testing.T) {
 | |
| 		n := &SNAT44{
 | |
| 			Machine:           m,
 | |
| 			ExternalInterface: wanIf,
 | |
| 			Type:              AddressAndPortDependentNAT,
 | |
| 			Firewall: &Firewall{
 | |
| 				TrustedInterface: lanIf,
 | |
| 			},
 | |
| 		}
 | |
| 		testNAT(t, n, lanIf, wanIf, []natTest{
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("2.2.2.2:5678"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("2.2.2.2:9012"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("7.7.7.7:9012"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 			{
 | |
| 				src:            ipp("192.168.0.20:1234"),
 | |
| 				dst:            ipp("7.7.7.7:1234"),
 | |
| 				wantNewMapping: true,
 | |
| 			},
 | |
| 		})
 | |
| 	})
 | |
| }
 | |
| 
 | |
| type natTest struct {
 | |
| 	src, dst       netip.AddrPort
 | |
| 	wantNewMapping bool
 | |
| }
 | |
| 
 | |
| func testNAT(t *testing.T, n *SNAT44, lanIf, wanIf *Interface, tests []natTest) {
 | |
| 	clock := &tstest.Clock{}
 | |
| 	n.TimeNow = clock.Now
 | |
| 
 | |
| 	mappings := map[netip.AddrPort]bool{}
 | |
| 	for _, test := range tests {
 | |
| 		clock.Advance(time.Second)
 | |
| 		p := &Packet{
 | |
| 			Src:     test.src,
 | |
| 			Dst:     test.dst,
 | |
| 			Payload: []byte("foo"),
 | |
| 		}
 | |
| 		gotPacket := n.HandleForward(p.Clone(), lanIf, wanIf)
 | |
| 		if gotPacket == nil {
 | |
| 			t.Errorf("n.HandleForward(%v) dropped packet", p)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if gotPacket.Dst != p.Dst {
 | |
| 			t.Errorf("n.HandleForward(%v) mutated dest ip:port, got %v", p, gotPacket.Dst)
 | |
| 		}
 | |
| 		gotNewMapping := !mappings[gotPacket.Src]
 | |
| 		if gotNewMapping != test.wantNewMapping {
 | |
| 			t.Errorf("n.HandleForward(%v) mapping was new=%v, want %v", p, gotNewMapping, test.wantNewMapping)
 | |
| 		}
 | |
| 		mappings[gotPacket.Src] = true
 | |
| 
 | |
| 		// Check that the return path works and translates back
 | |
| 		// correctly.
 | |
| 		clock.Advance(time.Second)
 | |
| 		p2 := &Packet{
 | |
| 			Src:     test.dst,
 | |
| 			Dst:     gotPacket.Src,
 | |
| 			Payload: []byte("bar"),
 | |
| 		}
 | |
| 		gotPacket2 := n.HandleIn(p2.Clone(), wanIf)
 | |
| 
 | |
| 		if gotPacket2 == nil {
 | |
| 			t.Errorf("return packet was dropped")
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if gotPacket2.Src != test.dst {
 | |
| 			t.Errorf("return packet has src=%v, want %v", gotPacket2.Src, test.dst)
 | |
| 		}
 | |
| 		if gotPacket2.Dst != test.src {
 | |
| 			t.Errorf("return packet has dst=%v, want %v", gotPacket2.Dst, test.src)
 | |
| 		}
 | |
| 	}
 | |
| }
 |