...

Source file src/k8s.io/kubernetes/pkg/proxy/util/nodeport_addresses_test.go

Documentation: k8s.io/kubernetes/pkg/proxy/util

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package util
    18  
    19  import (
    20  	"fmt"
    21  	"net"
    22  	"testing"
    23  
    24  	v1 "k8s.io/api/core/v1"
    25  	"k8s.io/apimachinery/pkg/util/sets"
    26  	fake "k8s.io/kubernetes/pkg/proxy/util/testing"
    27  	netutils "k8s.io/utils/net"
    28  )
    29  
    30  type InterfaceAddrsPair struct {
    31  	itf   net.Interface
    32  	addrs []net.Addr
    33  }
    34  
    35  func checkNodeIPs(expected sets.Set[string], actual []net.IP) error {
    36  	notFound := expected.Clone()
    37  	extra := sets.New[string]()
    38  	for _, ip := range actual {
    39  		str := ip.String()
    40  		if notFound.Has(str) {
    41  			notFound.Delete(str)
    42  		} else {
    43  			extra.Insert(str)
    44  		}
    45  	}
    46  	if len(notFound) != 0 || len(extra) != 0 {
    47  		return fmt.Errorf("not found: %v, extra: %v", notFound.UnsortedList(), extra.UnsortedList())
    48  	}
    49  	return nil
    50  }
    51  
    52  func TestGetNodeIPs(t *testing.T) {
    53  	type expectation struct {
    54  		matchAll bool
    55  		ips      sets.Set[string]
    56  	}
    57  
    58  	testCases := []struct {
    59  		name          string
    60  		cidrs         []string
    61  		itfAddrsPairs []InterfaceAddrsPair
    62  		expected      map[v1.IPFamily]expectation
    63  		// nodeIP will take effect when `--nodeport-addresses` is empty
    64  		nodeIP net.IP
    65  	}{
    66  		{
    67  			name:  "IPv4 single",
    68  			cidrs: []string{"10.20.30.0/24"},
    69  			itfAddrsPairs: []InterfaceAddrsPair{
    70  				{
    71  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
    72  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("10.20.30.51"), Mask: net.CIDRMask(24, 32)}},
    73  				},
    74  				{
    75  					itf:   net.Interface{Index: 2, MTU: 0, Name: "eth1", HardwareAddr: nil, Flags: 0},
    76  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("100.200.201.1"), Mask: net.CIDRMask(24, 32)}},
    77  				},
    78  			},
    79  			expected: map[v1.IPFamily]expectation{
    80  				v1.IPv4Protocol: {
    81  					ips: sets.New[string]("10.20.30.51"),
    82  				},
    83  				v1.IPv6Protocol: {
    84  					matchAll: true,
    85  					ips:      nil,
    86  				},
    87  			},
    88  		},
    89  		{
    90  			name:  "IPv4 zero CIDR",
    91  			cidrs: []string{"0.0.0.0/0"},
    92  			itfAddrsPairs: []InterfaceAddrsPair{
    93  				{
    94  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
    95  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("10.20.30.51"), Mask: net.CIDRMask(24, 32)}},
    96  				},
    97  				{
    98  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
    99  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)}},
   100  				},
   101  			},
   102  			expected: map[v1.IPFamily]expectation{
   103  				v1.IPv4Protocol: {
   104  					matchAll: true,
   105  					ips:      sets.New[string]("10.20.30.51", "127.0.0.1"),
   106  				},
   107  				v1.IPv6Protocol: {
   108  					matchAll: true,
   109  					ips:      nil,
   110  				},
   111  			},
   112  		},
   113  		{
   114  			name:  "IPv6 multiple",
   115  			cidrs: []string{"2001:db8::/64", "::1/128"},
   116  			itfAddrsPairs: []InterfaceAddrsPair{
   117  				{
   118  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   119  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("2001:db8::1"), Mask: net.CIDRMask(64, 128)}},
   120  				},
   121  				{
   122  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   123  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("::1"), Mask: net.CIDRMask(128, 128)}},
   124  				},
   125  			},
   126  			expected: map[v1.IPFamily]expectation{
   127  				v1.IPv4Protocol: {
   128  					matchAll: true,
   129  					ips:      nil,
   130  				},
   131  				v1.IPv6Protocol: {
   132  					ips: sets.New[string]("2001:db8::1", "::1"),
   133  				},
   134  			},
   135  		},
   136  		{
   137  			name:  "IPv6 zero CIDR",
   138  			cidrs: []string{"::/0"},
   139  			itfAddrsPairs: []InterfaceAddrsPair{
   140  				{
   141  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   142  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("2001:db8::1"), Mask: net.CIDRMask(64, 128)}},
   143  				},
   144  				{
   145  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   146  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("::1"), Mask: net.CIDRMask(128, 128)}},
   147  				},
   148  			},
   149  			expected: map[v1.IPFamily]expectation{
   150  				v1.IPv4Protocol: {
   151  					matchAll: true,
   152  					ips:      nil,
   153  				},
   154  				v1.IPv6Protocol: {
   155  					matchAll: true,
   156  					ips:      sets.New[string]("2001:db8::1", "::1"),
   157  				},
   158  			},
   159  		},
   160  		{
   161  			name:  "IPv4 localhost exact",
   162  			cidrs: []string{"127.0.0.1/32"},
   163  			itfAddrsPairs: []InterfaceAddrsPair{
   164  				{
   165  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   166  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("10.20.30.51"), Mask: net.CIDRMask(24, 32)}},
   167  				},
   168  				{
   169  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   170  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)}},
   171  				},
   172  			},
   173  			expected: map[v1.IPFamily]expectation{
   174  				v1.IPv4Protocol: {
   175  					ips: sets.New[string]("127.0.0.1"),
   176  				},
   177  				v1.IPv6Protocol: {
   178  					matchAll: true,
   179  					ips:      nil,
   180  				},
   181  			},
   182  		},
   183  		{
   184  			name:  "IPv4 localhost subnet",
   185  			cidrs: []string{"127.0.0.0/8"},
   186  			itfAddrsPairs: []InterfaceAddrsPair{
   187  				{
   188  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   189  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("127.0.1.1"), Mask: net.CIDRMask(8, 32)}},
   190  				},
   191  			},
   192  			expected: map[v1.IPFamily]expectation{
   193  				v1.IPv4Protocol: {
   194  					ips: sets.New[string]("127.0.1.1"),
   195  				},
   196  				v1.IPv6Protocol: {
   197  					matchAll: true,
   198  					ips:      nil,
   199  				},
   200  			},
   201  		},
   202  		{
   203  			name:  "IPv4 multiple",
   204  			cidrs: []string{"10.20.30.0/24", "100.200.201.0/24"},
   205  			itfAddrsPairs: []InterfaceAddrsPair{
   206  				{
   207  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   208  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("10.20.30.51"), Mask: net.CIDRMask(24, 32)}},
   209  				},
   210  				{
   211  					itf:   net.Interface{Index: 2, MTU: 0, Name: "eth1", HardwareAddr: nil, Flags: 0},
   212  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("100.200.201.1"), Mask: net.CIDRMask(24, 32)}},
   213  				},
   214  			},
   215  			expected: map[v1.IPFamily]expectation{
   216  				v1.IPv4Protocol: {
   217  					ips: sets.New[string]("10.20.30.51", "100.200.201.1"),
   218  				},
   219  				v1.IPv6Protocol: {
   220  					matchAll: true,
   221  					ips:      nil,
   222  				},
   223  			},
   224  		},
   225  		{
   226  			name:  "IPv4 multiple, no match",
   227  			cidrs: []string{"10.20.30.0/24", "100.200.201.0/24"},
   228  			itfAddrsPairs: []InterfaceAddrsPair{
   229  				{
   230  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   231  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("192.168.1.2"), Mask: net.CIDRMask(24, 32)}},
   232  				},
   233  				{
   234  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   235  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)}},
   236  				},
   237  			},
   238  			expected: map[v1.IPFamily]expectation{
   239  				v1.IPv4Protocol: {
   240  					ips: nil,
   241  				},
   242  				v1.IPv6Protocol: {
   243  					matchAll: true,
   244  					ips:      nil,
   245  				},
   246  			},
   247  		},
   248  		{
   249  			name:  "empty list, IPv4 addrs",
   250  			cidrs: []string{},
   251  			itfAddrsPairs: []InterfaceAddrsPair{
   252  				{
   253  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   254  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("192.168.1.2"), Mask: net.CIDRMask(24, 32)}},
   255  				},
   256  				{
   257  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   258  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)}},
   259  				},
   260  			},
   261  			expected: map[v1.IPFamily]expectation{
   262  				v1.IPv4Protocol: {
   263  					matchAll: true,
   264  					ips:      sets.New[string]("192.168.1.2", "127.0.0.1"),
   265  				},
   266  				v1.IPv6Protocol: {
   267  					matchAll: true,
   268  					ips:      nil,
   269  				},
   270  			},
   271  		},
   272  		{
   273  			name:  "empty list, IPv6 addrs",
   274  			cidrs: []string{},
   275  			itfAddrsPairs: []InterfaceAddrsPair{
   276  				{
   277  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   278  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("2001:db8::1"), Mask: net.CIDRMask(64, 128)}},
   279  				},
   280  				{
   281  					itf:   net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   282  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("::1"), Mask: net.CIDRMask(128, 128)}},
   283  				},
   284  			},
   285  			expected: map[v1.IPFamily]expectation{
   286  				v1.IPv4Protocol: {
   287  					matchAll: true,
   288  					ips:      nil,
   289  				},
   290  				v1.IPv6Protocol: {
   291  					matchAll: true,
   292  					ips:      sets.New[string]("2001:db8::1", "::1"),
   293  				},
   294  			},
   295  		},
   296  		{
   297  			name:  "IPv4 redundant CIDRs",
   298  			cidrs: []string{"1.2.3.0/24", "0.0.0.0/0"},
   299  			itfAddrsPairs: []InterfaceAddrsPair{
   300  				{
   301  					itf:   net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   302  					addrs: []net.Addr{&net.IPNet{IP: netutils.ParseIPSloppy("1.2.3.4"), Mask: net.CIDRMask(30, 32)}},
   303  				},
   304  			},
   305  			expected: map[v1.IPFamily]expectation{
   306  				v1.IPv4Protocol: {
   307  					matchAll: true,
   308  					ips:      sets.New[string]("1.2.3.4"),
   309  				},
   310  				v1.IPv6Protocol: {
   311  					matchAll: true,
   312  					ips:      nil,
   313  				},
   314  			},
   315  		},
   316  		{
   317  			name:  "Dual-stack, redundant IPv4",
   318  			cidrs: []string{"0.0.0.0/0", "1.2.3.0/24", "2001:db8::1/128"},
   319  			itfAddrsPairs: []InterfaceAddrsPair{
   320  				{
   321  					itf: net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   322  					addrs: []net.Addr{
   323  						&net.IPNet{IP: netutils.ParseIPSloppy("1.2.3.4"), Mask: net.CIDRMask(30, 32)},
   324  						&net.IPNet{IP: netutils.ParseIPSloppy("2001:db8::1"), Mask: net.CIDRMask(64, 128)},
   325  					},
   326  				},
   327  				{
   328  					itf: net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   329  					addrs: []net.Addr{
   330  						&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)},
   331  						&net.IPNet{IP: netutils.ParseIPSloppy("::1"), Mask: net.CIDRMask(128, 128)},
   332  					},
   333  				},
   334  			},
   335  			expected: map[v1.IPFamily]expectation{
   336  				v1.IPv4Protocol: {
   337  					matchAll: true,
   338  					ips:      sets.New[string]("1.2.3.4", "127.0.0.1"),
   339  				},
   340  				v1.IPv6Protocol: {
   341  					ips: sets.New[string]("2001:db8::1"),
   342  				},
   343  			},
   344  		},
   345  		{
   346  			name:  "Dual-stack, redundant IPv6",
   347  			cidrs: []string{"::/0", "1.2.3.0/24", "2001:db8::1/128"},
   348  			itfAddrsPairs: []InterfaceAddrsPair{
   349  				{
   350  					itf: net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   351  					addrs: []net.Addr{
   352  						&net.IPNet{IP: netutils.ParseIPSloppy("1.2.3.4"), Mask: net.CIDRMask(30, 32)},
   353  						&net.IPNet{IP: netutils.ParseIPSloppy("2001:db8::1"), Mask: net.CIDRMask(64, 128)},
   354  					},
   355  				},
   356  				{
   357  					itf: net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   358  					addrs: []net.Addr{
   359  						&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)},
   360  						&net.IPNet{IP: netutils.ParseIPSloppy("::1"), Mask: net.CIDRMask(128, 128)},
   361  					},
   362  				},
   363  			},
   364  			expected: map[v1.IPFamily]expectation{
   365  				v1.IPv4Protocol: {
   366  					ips: sets.New[string]("1.2.3.4"),
   367  				},
   368  				v1.IPv6Protocol: {
   369  					matchAll: true,
   370  					ips:      sets.New[string]("2001:db8::1", "::1"),
   371  				},
   372  			},
   373  		},
   374  		{
   375  			name: "ipv4 with nodeIP",
   376  			itfAddrsPairs: []InterfaceAddrsPair{
   377  				{
   378  					itf: net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   379  					addrs: []net.Addr{
   380  						&net.IPNet{IP: netutils.ParseIPSloppy("1.2.3.4"), Mask: net.CIDRMask(30, 32)},
   381  					},
   382  				},
   383  				{
   384  					itf: net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   385  					addrs: []net.Addr{
   386  						&net.IPNet{IP: netutils.ParseIPSloppy("127.0.0.1"), Mask: net.CIDRMask(8, 32)},
   387  					},
   388  				},
   389  			},
   390  			expected: map[v1.IPFamily]expectation{
   391  				v1.IPv4Protocol: {
   392  					ips: sets.New[string]("1.2.3.4"),
   393  				},
   394  			},
   395  			nodeIP: netutils.ParseIPSloppy("1.2.3.4"),
   396  		},
   397  		{
   398  			name: "ipv6 with nodeIP",
   399  			itfAddrsPairs: []InterfaceAddrsPair{
   400  				{
   401  					itf: net.Interface{Index: 0, MTU: 0, Name: "eth0", HardwareAddr: nil, Flags: 0},
   402  					addrs: []net.Addr{
   403  						&net.IPNet{IP: netutils.ParseIPSloppy("2001:db8::1"), Mask: net.CIDRMask(64, 128)},
   404  					},
   405  				},
   406  				{
   407  					itf: net.Interface{Index: 1, MTU: 0, Name: "lo", HardwareAddr: nil, Flags: 0},
   408  					addrs: []net.Addr{
   409  						&net.IPNet{IP: netutils.ParseIPSloppy("::1"), Mask: net.CIDRMask(128, 128)},
   410  					},
   411  				},
   412  			},
   413  			expected: map[v1.IPFamily]expectation{
   414  				v1.IPv6Protocol: {
   415  					matchAll: true,
   416  					ips:      sets.New[string]("2001:db8::1", "::1"),
   417  				},
   418  			},
   419  			nodeIP: netutils.ParseIPSloppy("1.2.3.4"),
   420  		},
   421  	}
   422  
   423  	for _, tc := range testCases {
   424  		t.Run(tc.name, func(t *testing.T) {
   425  			nw := fake.NewFakeNetwork()
   426  			for _, pair := range tc.itfAddrsPairs {
   427  				nw.AddInterfaceAddr(&pair.itf, pair.addrs)
   428  			}
   429  
   430  			for _, family := range []v1.IPFamily{v1.IPv4Protocol, v1.IPv6Protocol} {
   431  				if tc.nodeIP != nil && v1.IPFamily(fmt.Sprintf("IPv%s", netutils.IPFamilyOf(tc.nodeIP))) != family {
   432  					continue
   433  				}
   434  				npa := NewNodePortAddresses(family, tc.cidrs, tc.nodeIP)
   435  
   436  				if npa.MatchAll() != tc.expected[family].matchAll {
   437  					t.Errorf("unexpected MatchAll(%s), expected: %v", family, tc.expected[family].matchAll)
   438  				}
   439  
   440  				ips, err := npa.GetNodeIPs(nw)
   441  				expectedIPs := tc.expected[family].ips
   442  
   443  				// The fake InterfaceAddrs() never returns an error, so
   444  				// the only error GetNodeIPs will return is "no
   445  				// addresses found".
   446  				if err != nil {
   447  					t.Errorf("unexpected error: %v", err)
   448  				}
   449  				err = checkNodeIPs(expectedIPs, ips)
   450  				if err != nil {
   451  					t.Errorf("unexpected mismatch for %s: %v", family, err)
   452  				}
   453  			}
   454  		})
   455  	}
   456  }
   457  
   458  func TestContainsIPv4Loopback(t *testing.T) {
   459  	tests := []struct {
   460  		name        string
   461  		cidrStrings []string
   462  		want        bool
   463  	}{
   464  		{
   465  			name: "empty",
   466  			want: true,
   467  		},
   468  		{
   469  			name:        "all zeros ipv4",
   470  			cidrStrings: []string{"224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64", "0.0.0.0/0"},
   471  			want:        true,
   472  		},
   473  		{
   474  			name:        "all zeros ipv6",
   475  			cidrStrings: []string{"224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64", "::/0"},
   476  			want:        false,
   477  		},
   478  		{
   479  			name:        "ipv4 loopback",
   480  			cidrStrings: []string{"224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64", "127.0.0.0/8"},
   481  			want:        true,
   482  		},
   483  		{
   484  			name:        "ipv6 loopback",
   485  			cidrStrings: []string{"224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64", "::1/128"},
   486  			want:        false,
   487  		},
   488  		{
   489  			name:        "ipv4 loopback smaller range",
   490  			cidrStrings: []string{"224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64", "127.0.2.0/28"},
   491  			want:        true,
   492  		},
   493  		{
   494  			name:        "ipv4 loopback within larger range",
   495  			cidrStrings: []string{"224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64", "64.0.0.0/2"},
   496  			want:        true,
   497  		},
   498  		{
   499  			name:        "non loop loopback",
   500  			cidrStrings: []string{"128.0.2.0/28", "224.0.0.0/24", "192.168.0.0/16", "fd00:1:d::/64"},
   501  			want:        false,
   502  		},
   503  	}
   504  	for _, tt := range tests {
   505  		t.Run(tt.name, func(t *testing.T) {
   506  			npa := NewNodePortAddresses(v1.IPv4Protocol, tt.cidrStrings, nil)
   507  			if got := npa.ContainsIPv4Loopback(); got != tt.want {
   508  				t.Errorf("IPv4 ContainsIPv4Loopback() = %v, want %v", got, tt.want)
   509  			}
   510  			// ContainsIPv4Loopback should always be false for family=IPv6
   511  			npa = NewNodePortAddresses(v1.IPv6Protocol, tt.cidrStrings, nil)
   512  			if got := npa.ContainsIPv4Loopback(); got {
   513  				t.Errorf("IPv6 ContainsIPv4Loopback() = %v, want %v", got, false)
   514  			}
   515  		})
   516  	}
   517  }
   518  

View as plain text