Unexpose immutable fields in stack.Route

This change sets the inner `routeInfo` struct to be a named private member
and replaces direct access with access through getters. Note that direct
access to the fields of `routeInfo` is still possible through the `RouteInfo`
struct.

Fixes #4902

PiperOrigin-RevId: 364822872
diff --git a/pkg/tcpip/network/ipv4/icmp.go b/pkg/tcpip/network/ipv4/icmp.go
index deb1048..1525f15 100644
--- a/pkg/tcpip/network/ipv4/icmp.go
+++ b/pkg/tcpip/network/ipv4/icmp.go
@@ -305,8 +305,8 @@
 		// We need to produce the entire packet in the data segment in order to
 		// use WriteHeaderIncludedPacket(). WriteHeaderIncludedPacket sets the
 		// total length and the header checksum so we don't need to set those here.
-		replyIPHdr.SetSourceAddress(r.LocalAddress)
-		replyIPHdr.SetDestinationAddress(r.RemoteAddress)
+		replyIPHdr.SetSourceAddress(r.LocalAddress())
+		replyIPHdr.SetDestinationAddress(r.RemoteAddress())
 		replyIPHdr.SetTTL(r.DefaultTTL())
 
 		replyICMPHdr := header.ICMPv4(replyData)
diff --git a/pkg/tcpip/network/ipv4/ipv4.go b/pkg/tcpip/network/ipv4/ipv4.go
index a1660e9..1a5661c 100644
--- a/pkg/tcpip/network/ipv4/ipv4.go
+++ b/pkg/tcpip/network/ipv4/ipv4.go
@@ -339,7 +339,7 @@
 
 // WritePacket writes a packet to the given destination address and protocol.
 func (e *endpoint) WritePacket(r *stack.Route, gso *stack.GSO, params stack.NetworkHeaderParams, pkt *stack.PacketBuffer) tcpip.Error {
-	if err := e.addIPHeader(r.LocalAddress, r.RemoteAddress, pkt, params, nil /* options */); err != nil {
+	if err := e.addIPHeader(r.LocalAddress(), r.RemoteAddress(), pkt, params, nil /* options */); err != nil {
 		return err
 	}
 
@@ -373,13 +373,13 @@
 }
 
 func (e *endpoint) writePacket(r *stack.Route, gso *stack.GSO, pkt *stack.PacketBuffer, headerIncluded bool) tcpip.Error {
-	if r.Loop&stack.PacketLoop != 0 {
+	if r.Loop()&stack.PacketLoop != 0 {
 		// If the packet was generated by the stack (not a raw/packet endpoint
 		// where a packet may be written with the header included), then we can
 		// safely assume the checksum is valid.
 		e.handleLocalPacket(pkt, !headerIncluded /* canSkipRXChecksum */)
 	}
-	if r.Loop&stack.PacketOut == 0 {
+	if r.Loop()&stack.PacketOut == 0 {
 		return nil
 	}
 
@@ -414,17 +414,17 @@
 
 // WritePackets implements stack.NetworkEndpoint.WritePackets.
 func (e *endpoint) WritePackets(r *stack.Route, gso *stack.GSO, pkts stack.PacketBufferList, params stack.NetworkHeaderParams) (int, tcpip.Error) {
-	if r.Loop&stack.PacketLoop != 0 {
+	if r.Loop()&stack.PacketLoop != 0 {
 		panic("multiple packets in local loop")
 	}
-	if r.Loop&stack.PacketOut == 0 {
+	if r.Loop()&stack.PacketOut == 0 {
 		return pkts.Len(), nil
 	}
 
 	stats := e.stats.ip
 
 	for pkt := pkts.Front(); pkt != nil; pkt = pkt.Next() {
-		if err := e.addIPHeader(r.LocalAddress, r.RemoteAddress, pkt, params, nil /* options */); err != nil {
+		if err := e.addIPHeader(r.LocalAddress(), r.RemoteAddress(), pkt, params, nil /* options */); err != nil {
 			return 0, err
 		}
 
@@ -514,12 +514,12 @@
 
 	// Set the source address when zero.
 	if ip.SourceAddress() == header.IPv4Any {
-		ip.SetSourceAddress(r.LocalAddress)
+		ip.SetSourceAddress(r.LocalAddress())
 	}
 
 	// Set the destination. If the packet already included a destination, it will
 	// be part of the route anyways.
-	ip.SetDestinationAddress(r.RemoteAddress)
+	ip.SetDestinationAddress(r.RemoteAddress())
 
 	// Set the packet ID when zero.
 	if ip.ID() == 0 {
@@ -527,7 +527,7 @@
 		// non-atomic datagrams, so assign an ID to all such datagrams
 		// according to the definition given in RFC 6864 section 4.
 		if ip.Flags()&header.IPv4FlagDontFragment == 0 || ip.Flags()&header.IPv4FlagMoreFragments != 0 || ip.FragmentOffset() > 0 {
-			ip.SetID(uint16(atomic.AddUint32(&e.protocol.ids[hashRoute(r.LocalAddress, r.RemoteAddress, 0 /* protocol */, e.protocol.hashIV)%buckets], 1)))
+			ip.SetID(uint16(atomic.AddUint32(&e.protocol.ids[hashRoute(r.LocalAddress(), r.RemoteAddress(), 0 /* protocol */, e.protocol.hashIV)%buckets], 1)))
 		}
 	}
 
diff --git a/pkg/tcpip/network/ipv6/icmp.go b/pkg/tcpip/network/ipv6/icmp.go
index 2afa856..a142b76 100644
--- a/pkg/tcpip/network/ipv6/icmp.go
+++ b/pkg/tcpip/network/ipv6/icmp.go
@@ -554,8 +554,8 @@
 		na.Options().Serialize(optsSerializer)
 		packet.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
 			Header: packet,
-			Src:    r.LocalAddress,
-			Dst:    r.RemoteAddress,
+			Src:    r.LocalAddress(),
+			Dst:    r.RemoteAddress(),
 		}))
 
 		// RFC 4861 Neighbor Discovery for IP version 6 (IPv6)
@@ -699,8 +699,8 @@
 		dataRange := replyPkt.Data().AsRange()
 		icmp.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
 			Header:      icmp,
-			Src:         r.LocalAddress,
-			Dst:         r.RemoteAddress,
+			Src:         r.LocalAddress(),
+			Dst:         r.RemoteAddress(),
 			PayloadCsum: dataRange.Checksum(),
 			PayloadLen:  dataRange.Size(),
 		}))
@@ -1161,8 +1161,8 @@
 	dataRange := newPkt.Data().AsRange()
 	icmpHdr.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
 		Header:      icmpHdr,
-		Src:         route.LocalAddress,
-		Dst:         route.RemoteAddress,
+		Src:         route.LocalAddress(),
+		Dst:         route.RemoteAddress(),
 		PayloadCsum: dataRange.Checksum(),
 		PayloadLen:  dataRange.Size(),
 	}))
diff --git a/pkg/tcpip/network/ipv6/icmp_test.go b/pkg/tcpip/network/ipv6/icmp_test.go
index 47d713f..6a7705e 100644
--- a/pkg/tcpip/network/ipv6/icmp_test.go
+++ b/pkg/tcpip/network/ipv6/icmp_test.go
@@ -510,8 +510,8 @@
 	pkt.SetType(header.ICMPv6EchoRequest)
 	pkt.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
 		Header: pkt,
-		Src:    r.LocalAddress,
-		Dst:    r.RemoteAddress,
+		Src:    r.LocalAddress(),
+		Dst:    r.RemoteAddress(),
 	}))
 
 	// We can't send our payload directly over the route because that
diff --git a/pkg/tcpip/network/ipv6/ipv6.go b/pkg/tcpip/network/ipv6/ipv6.go
index 83e98ba..c6d9d8f 100644
--- a/pkg/tcpip/network/ipv6/ipv6.go
+++ b/pkg/tcpip/network/ipv6/ipv6.go
@@ -682,7 +682,7 @@
 
 // WritePacket writes a packet to the given destination address and protocol.
 func (e *endpoint) WritePacket(r *stack.Route, gso *stack.GSO, params stack.NetworkHeaderParams, pkt *stack.PacketBuffer) tcpip.Error {
-	if err := addIPHeader(r.LocalAddress, r.RemoteAddress, pkt, params, nil /* extensionHeaders */); err != nil {
+	if err := addIPHeader(r.LocalAddress(), r.RemoteAddress(), pkt, params, nil /* extensionHeaders */); err != nil {
 		return err
 	}
 
@@ -716,13 +716,13 @@
 }
 
 func (e *endpoint) writePacket(r *stack.Route, gso *stack.GSO, pkt *stack.PacketBuffer, protocol tcpip.TransportProtocolNumber, headerIncluded bool) tcpip.Error {
-	if r.Loop&stack.PacketLoop != 0 {
+	if r.Loop()&stack.PacketLoop != 0 {
 		// If the packet was generated by the stack (not a raw/packet endpoint
 		// where a packet may be written with the header included), then we can
 		// safely assume the checksum is valid.
 		e.handleLocalPacket(pkt, !headerIncluded /* canSkipRXChecksum */)
 	}
-	if r.Loop&stack.PacketOut == 0 {
+	if r.Loop()&stack.PacketOut == 0 {
 		return nil
 	}
 
@@ -757,17 +757,17 @@
 
 // WritePackets implements stack.NetworkEndpoint.WritePackets.
 func (e *endpoint) WritePackets(r *stack.Route, gso *stack.GSO, pkts stack.PacketBufferList, params stack.NetworkHeaderParams) (int, tcpip.Error) {
-	if r.Loop&stack.PacketLoop != 0 {
+	if r.Loop()&stack.PacketLoop != 0 {
 		panic("not implemented")
 	}
-	if r.Loop&stack.PacketOut == 0 {
+	if r.Loop()&stack.PacketOut == 0 {
 		return pkts.Len(), nil
 	}
 
 	stats := e.stats.ip
 	linkMTU := e.nic.MTU()
 	for pb := pkts.Front(); pb != nil; pb = pb.Next() {
-		if err := addIPHeader(r.LocalAddress, r.RemoteAddress, pb, params, nil /* extensionHeaders */); err != nil {
+		if err := addIPHeader(r.LocalAddress(), r.RemoteAddress(), pb, params, nil /* extensionHeaders */); err != nil {
 			return 0, err
 		}
 
@@ -845,12 +845,12 @@
 
 	// Set the source address when zero.
 	if ip.SourceAddress() == header.IPv6Any {
-		ip.SetSourceAddress(r.LocalAddress)
+		ip.SetSourceAddress(r.LocalAddress())
 	}
 
 	// Set the destination. If the packet already included a destination, it will
 	// be part of the route anyways.
-	ip.SetDestinationAddress(r.RemoteAddress)
+	ip.SetDestinationAddress(r.RemoteAddress())
 
 	// Populate the packet buffer's network header and don't allow an invalid
 	// packet to be sent.
@@ -2113,11 +2113,11 @@
 	// The FNV-1a was chosen because it is a fast hashing algorithm, and
 	// cryptographic properties are not needed here.
 	h := fnv.New32a()
-	if _, err := h.Write([]byte(r.LocalAddress)); err != nil {
+	if _, err := h.Write([]byte(r.LocalAddress())); err != nil {
 		panic(fmt.Sprintf("Hash.Write: %s, but Hash' implementation of Write is not expected to ever return an error", err))
 	}
 
-	if _, err := h.Write([]byte(r.RemoteAddress)); err != nil {
+	if _, err := h.Write([]byte(r.RemoteAddress())); err != nil {
 		panic(fmt.Sprintf("Hash.Write: %s, but Hash' implementation of Write is not expected to ever return an error", err))
 	}
 
diff --git a/pkg/tcpip/stack/forwarding_test.go b/pkg/tcpip/stack/forwarding_test.go
index c987c18..16ee75b 100644
--- a/pkg/tcpip/stack/forwarding_test.go
+++ b/pkg/tcpip/stack/forwarding_test.go
@@ -121,8 +121,8 @@
 	// Add the protocol's header to the packet and send it to the link
 	// endpoint.
 	b := pkt.NetworkHeader().Push(fwdTestNetHeaderLen)
-	b[dstAddrOffset] = r.RemoteAddress[0]
-	b[srcAddrOffset] = r.LocalAddress[0]
+	b[dstAddrOffset] = r.RemoteAddress()[0]
+	b[srcAddrOffset] = r.LocalAddress()[0]
 	b[protocolNumberOffset] = byte(params.Protocol)
 
 	return f.nic.WritePacket(r, gso, fwdTestNetNumber, pkt)
diff --git a/pkg/tcpip/stack/ndp_test.go b/pkg/tcpip/stack/ndp_test.go
index 43e6d10..0725e02 100644
--- a/pkg/tcpip/stack/ndp_test.go
+++ b/pkg/tcpip/stack/ndp_test.go
@@ -578,8 +578,8 @@
 				r, err := s.FindRoute(nicID, "", addr2, header.IPv6ProtocolNumber, false)
 				if err != nil {
 					t.Errorf("got FindRoute(%d, '', %s, %d, false): %s", nicID, addr2, header.IPv6ProtocolNumber, err)
-				} else if r.LocalAddress != addr1 {
-					t.Errorf("got r.LocalAddress = %s, want = %s", r.LocalAddress, addr1)
+				} else if r.LocalAddress() != addr1 {
+					t.Errorf("got r.LocalAddress() = %s, want = %s", r.LocalAddress(), addr1)
 				}
 				r.Release()
 			}
@@ -587,8 +587,8 @@
 				r, err := s.FindRoute(nicID, addr1, addr2, header.IPv6ProtocolNumber, false)
 				if err != nil {
 					t.Errorf("got FindRoute(%d, %s, %s, %d, false): %s", nicID, addr1, addr2, header.IPv6ProtocolNumber, err)
-				} else if r.LocalAddress != addr1 {
-					t.Errorf("got r.LocalAddress = %s, want = %s", r.LocalAddress, addr1)
+				} else if r.LocalAddress() != addr1 {
+					t.Errorf("got r.LocalAddress() = %s, want = %s", r.LocalAddress(), addr1)
 				}
 				if r != nil {
 					r.Release()
diff --git a/pkg/tcpip/stack/route.go b/pkg/tcpip/stack/route.go
index 4ba6794..3934480 100644
--- a/pkg/tcpip/stack/route.go
+++ b/pkg/tcpip/stack/route.go
@@ -25,12 +25,8 @@
 // Route represents a route through the networking stack to a given destination.
 //
 // It is safe to call Route's methods from multiple goroutines.
-//
-// The exported fields are immutable.
-//
-// TODO(gvisor.dev/issue/4902): Unexpose immutable fields.
 type Route struct {
-	routeInfo
+	routeInfo routeInfo
 
 	// localAddressNIC is the interface the address is associated with.
 	// TODO(gvisor.dev/issue/4548): Remove this field once we can query the
@@ -57,26 +53,49 @@
 }
 
 type routeInfo struct {
-	// RemoteAddress is the final destination of the route.
 	RemoteAddress tcpip.Address
 
-	// LocalAddress is the local address where the route starts.
 	LocalAddress tcpip.Address
 
-	// LocalLinkAddress is the link-layer (MAC) address of the
-	// where the route starts.
 	LocalLinkAddress tcpip.LinkAddress
 
-	// NextHop is the next node in the path to the destination.
 	NextHop tcpip.Address
 
-	// NetProto is the network-layer protocol.
 	NetProto tcpip.NetworkProtocolNumber
 
-	// Loop controls where WritePacket should send packets.
 	Loop PacketLooping
 }
 
+// RemoteAddress returns the route's destination.
+func (r *Route) RemoteAddress() tcpip.Address {
+	return r.routeInfo.RemoteAddress
+}
+
+// LocalAddress returns the route's local address.
+func (r *Route) LocalAddress() tcpip.Address {
+	return r.routeInfo.LocalAddress
+}
+
+// LocalLinkAddress returns the route's local link-layer address.
+func (r *Route) LocalLinkAddress() tcpip.LinkAddress {
+	return r.routeInfo.LocalLinkAddress
+}
+
+// NextHop returns the next node in the route's path to the destination.
+func (r *Route) NextHop() tcpip.Address {
+	return r.routeInfo.NextHop
+}
+
+// NetProto returns the route's network-layer protocol number.
+func (r *Route) NetProto() tcpip.NetworkProtocolNumber {
+	return r.routeInfo.NetProto
+}
+
+// Loop returns the route's required packet looping.
+func (r *Route) Loop() PacketLooping {
+	return r.routeInfo.Loop
+}
+
 // RouteInfo contains all of Route's exported fields.
 type RouteInfo struct {
 	routeInfo
@@ -167,20 +186,20 @@
 	}
 
 	r := makeRouteInner(netProto, localAddr, remoteAddr, outgoingNIC, localAddressNIC, localAddressEndpoint, loop)
-	if r.Loop&PacketOut == 0 {
+	if r.Loop()&PacketOut == 0 {
 		// Packet will not leave the stack, no need for a gateway or a remote link
 		// address.
 		return r
 	}
 
 	if r.outgoingNIC.LinkEndpoint.Capabilities()&CapabilityResolutionRequired != 0 {
-		if linkRes, ok := r.outgoingNIC.linkAddrResolvers[r.NetProto]; ok {
+		if linkRes, ok := r.outgoingNIC.linkAddrResolvers[r.NetProto()]; ok {
 			r.linkRes = linkRes
 		}
 	}
 
 	if len(gateway) > 0 {
-		r.NextHop = gateway
+		r.routeInfo.NextHop = gateway
 		return r
 	}
 
@@ -188,7 +207,7 @@
 		return r
 	}
 
-	if linkAddr, ok := r.linkRes.resolver.ResolveStaticAddress(r.RemoteAddress); ok {
+	if linkAddr, ok := r.linkRes.resolver.ResolveStaticAddress(r.RemoteAddress()); ok {
 		r.ResolveWith(linkAddr)
 		return r
 	}
@@ -198,9 +217,9 @@
 		return r
 	}
 
-	if r.RemoteAddress == r.LocalAddress {
+	if r.RemoteAddress() == r.LocalAddress() {
 		// Local link address is already known.
-		r.ResolveWith(r.LocalLinkAddress)
+		r.ResolveWith(r.LocalLinkAddress())
 	}
 
 	return r
@@ -256,7 +275,7 @@
 
 // MaxHeaderLength forwards the call to the network endpoint's implementation.
 func (r *Route) MaxHeaderLength() uint16 {
-	return r.outgoingNIC.getNetworkEndpoint(r.NetProto).MaxHeaderLength()
+	return r.outgoingNIC.getNetworkEndpoint(r.NetProto()).MaxHeaderLength()
 }
 
 // Stats returns a mutable copy of current stats.
@@ -267,7 +286,7 @@
 // PseudoHeaderChecksum forwards the call to the network endpoint's
 // implementation.
 func (r *Route) PseudoHeaderChecksum(protocol tcpip.TransportProtocolNumber, totalLen uint16) uint16 {
-	return header.PseudoHeaderChecksum(protocol, r.LocalAddress, r.RemoteAddress, totalLen)
+	return header.PseudoHeaderChecksum(protocol, r.LocalAddress(), r.RemoteAddress(), totalLen)
 }
 
 // RequiresTXTransportChecksum returns false if the route does not require
@@ -358,7 +377,7 @@
 	// address on the outgoing interface.
 	var linkAddressResolutionRequestLocalAddr tcpip.Address
 	if r.localAddressNIC == r.outgoingNIC {
-		linkAddressResolutionRequestLocalAddr = r.LocalAddress
+		linkAddressResolutionRequestLocalAddr = r.LocalAddress()
 	}
 
 	afterResolveFields := fields
@@ -378,15 +397,15 @@
 }
 
 func (r *Route) nextHop() tcpip.Address {
-	if len(r.NextHop) == 0 {
-		return r.RemoteAddress
+	if len(r.NextHop()) == 0 {
+		return r.RemoteAddress()
 	}
-	return r.NextHop
+	return r.NextHop()
 }
 
 // local returns true if the route is a local route.
 func (r *Route) local() bool {
-	return r.Loop == PacketLoop || r.outgoingNIC.IsLoopback()
+	return r.Loop() == PacketLoop || r.outgoingNIC.IsLoopback()
 }
 
 // IsResolutionRequired returns true if Resolve() must be called to resolve
@@ -421,7 +440,7 @@
 
 	// If the source NIC and outgoing NIC are different, make sure the stack has
 	// forwarding enabled, or the packet will be handled locally.
-	if r.outgoingNIC != r.localAddressNIC && !r.outgoingNIC.stack.Forwarding(r.NetProto) && (!r.outgoingNIC.stack.handleLocal || !r.outgoingNIC.hasAddress(r.NetProto, r.RemoteAddress)) {
+	if r.outgoingNIC != r.localAddressNIC && !r.outgoingNIC.stack.Forwarding(r.NetProto()) && (!r.outgoingNIC.stack.handleLocal || !r.outgoingNIC.hasAddress(r.NetProto(), r.RemoteAddress())) {
 		return false
 	}
 
@@ -434,7 +453,7 @@
 		return &tcpip.ErrInvalidEndpointState{}
 	}
 
-	return r.outgoingNIC.getNetworkEndpoint(r.NetProto).WritePacket(r, gso, params, pkt)
+	return r.outgoingNIC.getNetworkEndpoint(r.NetProto()).WritePacket(r, gso, params, pkt)
 }
 
 // WritePackets writes a list of n packets through the given route and returns
@@ -444,7 +463,7 @@
 		return 0, &tcpip.ErrInvalidEndpointState{}
 	}
 
-	return r.outgoingNIC.getNetworkEndpoint(r.NetProto).WritePackets(r, gso, pkts, params)
+	return r.outgoingNIC.getNetworkEndpoint(r.NetProto()).WritePackets(r, gso, pkts, params)
 }
 
 // WriteHeaderIncludedPacket writes a packet already containing a network
@@ -454,17 +473,17 @@
 		return &tcpip.ErrInvalidEndpointState{}
 	}
 
-	return r.outgoingNIC.getNetworkEndpoint(r.NetProto).WriteHeaderIncludedPacket(r, pkt)
+	return r.outgoingNIC.getNetworkEndpoint(r.NetProto()).WriteHeaderIncludedPacket(r, pkt)
 }
 
 // DefaultTTL returns the default TTL of the underlying network endpoint.
 func (r *Route) DefaultTTL() uint8 {
-	return r.outgoingNIC.getNetworkEndpoint(r.NetProto).DefaultTTL()
+	return r.outgoingNIC.getNetworkEndpoint(r.NetProto()).DefaultTTL()
 }
 
 // MTU returns the MTU of the underlying network endpoint.
 func (r *Route) MTU() uint32 {
-	return r.outgoingNIC.getNetworkEndpoint(r.NetProto).MTU()
+	return r.outgoingNIC.getNetworkEndpoint(r.NetProto()).MTU()
 }
 
 // Release decrements the reference counter of the resources associated with the
@@ -489,7 +508,7 @@
 func (r *Route) acquireLocked() {
 	if ep := r.mu.localAddressEndpoint; ep != nil {
 		if !ep.IncRef() {
-			panic(fmt.Sprintf("failed to increment reference count for local address endpoint = %s", r.LocalAddress))
+			panic(fmt.Sprintf("failed to increment reference count for local address endpoint = %s", r.LocalAddress()))
 		}
 	}
 }
@@ -519,7 +538,7 @@
 // packet.
 func (r *Route) IsOutboundBroadcast() bool {
 	// Only IPv4 has a notion of broadcast.
-	return r.isV4Broadcast(r.RemoteAddress)
+	return r.isV4Broadcast(r.RemoteAddress())
 }
 
 // ConfirmReachable informs the network/link layer that the neighbour used for
diff --git a/pkg/tcpip/stack/stack_test.go b/pkg/tcpip/stack/stack_test.go
index 7ddf7a0..2814b94 100644
--- a/pkg/tcpip/stack/stack_test.go
+++ b/pkg/tcpip/stack/stack_test.go
@@ -172,20 +172,20 @@
 
 func (f *fakeNetworkEndpoint) WritePacket(r *stack.Route, gso *stack.GSO, params stack.NetworkHeaderParams, pkt *stack.PacketBuffer) tcpip.Error {
 	// Increment the sent packet count in the protocol descriptor.
-	f.proto.sendPacketCount[int(r.RemoteAddress[0])%len(f.proto.sendPacketCount)]++
+	f.proto.sendPacketCount[int(r.RemoteAddress()[0])%len(f.proto.sendPacketCount)]++
 
 	// Add the protocol's header to the packet and send it to the link
 	// endpoint.
 	hdr := pkt.NetworkHeader().Push(fakeNetHeaderLen)
 	pkt.NetworkProtocolNumber = fakeNetNumber
-	hdr[dstAddrOffset] = r.RemoteAddress[0]
-	hdr[srcAddrOffset] = r.LocalAddress[0]
+	hdr[dstAddrOffset] = r.RemoteAddress()[0]
+	hdr[srcAddrOffset] = r.LocalAddress()[0]
 	hdr[protocolNumberOffset] = byte(params.Protocol)
 
-	if r.Loop&stack.PacketLoop != 0 {
+	if r.Loop()&stack.PacketLoop != 0 {
 		f.HandlePacket(pkt.Clone())
 	}
-	if r.Loop&stack.PacketOut == 0 {
+	if r.Loop()&stack.PacketOut == 0 {
 		return nil
 	}
 
@@ -597,12 +597,12 @@
 
 	defer r.Release()
 
-	if r.LocalAddress != expectedSrcAddr {
-		t.Fatalf("Bad source address: expected %v, got %v", expectedSrcAddr, r.LocalAddress)
+	if r.LocalAddress() != expectedSrcAddr {
+		t.Fatalf("got Route.LocalAddress() = %s, want = %s", expectedSrcAddr, r.LocalAddress())
 	}
 
-	if r.RemoteAddress != dstAddr {
-		t.Fatalf("Bad destination address: expected %v, got %v", dstAddr, r.RemoteAddress)
+	if r.RemoteAddress() != dstAddr {
+		t.Fatalf("got Route.RemoteAddress() = %s, want = %s", dstAddr, r.RemoteAddress())
 	}
 }
 
@@ -1451,11 +1451,11 @@
 					}
 					defer r.Release()
 
-					if r.LocalAddress != localAddr {
-						t.Errorf("got r.LocalAddress = %s, want = %s", r.LocalAddress, localAddr)
+					if r.LocalAddress() != localAddr {
+						t.Errorf("got r.LocalAddress() = %s, want = %s", r.LocalAddress(), localAddr)
 					}
-					if r.RemoteAddress != dstAddr {
-						t.Errorf("got r.RemoteAddress = %s, want = %s", r.RemoteAddress, dstAddr)
+					if r.RemoteAddress() != dstAddr {
+						t.Errorf("got r.RemoteAddress() = %s, want = %s", r.RemoteAddress(), dstAddr)
 					}
 
 					if n := ep.Drain(); n != 0 {
@@ -1522,11 +1522,11 @@
 	if err != nil {
 		t.Fatal("FindRoute failed:", err)
 	}
-	if r.LocalAddress != nonExistentLocalAddr {
-		t.Errorf("got Route.LocalAddress = %s, want = %s", r.LocalAddress, nonExistentLocalAddr)
+	if r.LocalAddress() != nonExistentLocalAddr {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), nonExistentLocalAddr)
 	}
-	if r.RemoteAddress != dstAddr {
-		t.Errorf("got Route.RemoteAddress = %s, want = %s", r.RemoteAddress, dstAddr)
+	if r.RemoteAddress() != dstAddr {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), dstAddr)
 	}
 	// Sending a packet works.
 	testSendTo(t, s, dstAddr, ep, nil)
@@ -1537,11 +1537,11 @@
 	if err != nil {
 		t.Fatal("FindRoute failed:", err)
 	}
-	if r.LocalAddress != localAddr {
-		t.Errorf("got Route.LocalAddress = %s, want = %s", r.LocalAddress, nonExistentLocalAddr)
+	if r.LocalAddress() != localAddr {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), nonExistentLocalAddr)
 	}
-	if r.RemoteAddress != dstAddr {
-		t.Errorf("got Route.RemoteAddress = %s, want = %s", r.RemoteAddress, dstAddr)
+	if r.RemoteAddress() != dstAddr {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), dstAddr)
 	}
 	// Sending a packet using the route works.
 	testSend(t, r, ep, nil)
@@ -1586,33 +1586,17 @@
 	if err != nil {
 		t.Fatal("FindRoute failed:", err)
 	}
-	if r.LocalAddress != nonExistentLocalAddr {
-		t.Errorf("got Route.LocalAddress = %s, want = %s", r.LocalAddress, nonExistentLocalAddr)
+	if r.LocalAddress() != nonExistentLocalAddr {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), nonExistentLocalAddr)
 	}
-	if r.RemoteAddress != dstAddr {
-		t.Errorf("got Route.RemoteAddress = %s, want = %s", r.RemoteAddress, dstAddr)
+	if r.RemoteAddress() != dstAddr {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), dstAddr)
 	}
 	// Sending a packet works.
 	// FIXME(b/139841518):Spoofing doesn't work if there is no primary address.
 	// testSendTo(t, s, remoteAddr, ep, nil)
 }
 
-func verifyRoute(gotRoute, wantRoute *stack.Route) error {
-	if gotRoute.LocalAddress != wantRoute.LocalAddress {
-		return fmt.Errorf("bad local address: got %s, want = %s", gotRoute.LocalAddress, wantRoute.LocalAddress)
-	}
-	if gotRoute.RemoteAddress != wantRoute.RemoteAddress {
-		return fmt.Errorf("bad remote address: got %s, want = %s", gotRoute.RemoteAddress, wantRoute.RemoteAddress)
-	}
-	if got, want := gotRoute.RemoteLinkAddress(), wantRoute.RemoteLinkAddress(); got != want {
-		return fmt.Errorf("bad remote link address: got %s, want = %s", got, want)
-	}
-	if gotRoute.NextHop != wantRoute.NextHop {
-		return fmt.Errorf("bad next-hop address: got %s, want = %s", gotRoute.NextHop, wantRoute.NextHop)
-	}
-	return nil
-}
-
 func TestOutgoingBroadcastWithEmptyRouteTable(t *testing.T) {
 	s := stack.New(stack.Options{
 		NetworkProtocols: []stack.NetworkProtocolFactory{fakeNetFactory},
@@ -1640,11 +1624,12 @@
 	if err != nil {
 		t.Fatalf("FindRoute(1, %v, %v, %d) failed: %v", header.IPv4Any, header.IPv4Broadcast, fakeNetNumber, err)
 	}
-	var wantRoute stack.Route
-	wantRoute.LocalAddress = header.IPv4Any
-	wantRoute.RemoteAddress = header.IPv4Broadcast
-	if err := verifyRoute(r, &wantRoute); err != nil {
-		t.Errorf("FindRoute(1, %v, %v, %d) returned unexpected Route: %v", header.IPv4Any, header.IPv4Broadcast, fakeNetNumber, err)
+	if r.LocalAddress() != header.IPv4Any {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), header.IPv4Any)
+	}
+
+	if r.RemoteAddress() != header.IPv4Broadcast {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), header.IPv4Broadcast)
 	}
 
 	// If the NIC doesn't exist, it won't work.
@@ -1700,11 +1685,12 @@
 	if err != nil {
 		t.Fatalf("FindRoute(1, %v, %v, %d) failed: %v", nic1Addr.Address, header.IPv4Broadcast, fakeNetNumber, err)
 	}
-	var wantRoute stack.Route
-	wantRoute.LocalAddress = nic1Addr.Address
-	wantRoute.RemoteAddress = header.IPv4Broadcast
-	if err := verifyRoute(r, &wantRoute); err != nil {
-		t.Errorf("FindRoute(1, %v, %v, %d) returned unexpected Route: %v", nic1Addr.Address, header.IPv4Broadcast, fakeNetNumber, err)
+	if r.LocalAddress() != nic1Addr.Address {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), nic1Addr.Address)
+	}
+
+	if r.RemoteAddress() != header.IPv4Broadcast {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), header.IPv4Broadcast)
 	}
 
 	// When an interface is not given, it consults the route table.
@@ -1713,11 +1699,12 @@
 	if err != nil {
 		t.Fatalf("FindRoute(0, \"\", %s, %d) failed: %s", header.IPv4Broadcast, fakeNetNumber, err)
 	}
-	wantRoute = stack.Route{}
-	wantRoute.LocalAddress = nic2Addr.Address
-	wantRoute.RemoteAddress = header.IPv4Broadcast
-	if err := verifyRoute(r, &wantRoute); err != nil {
-		t.Errorf("FindRoute(0, \"\", %s, %d) returned unexpected Route: %s)", header.IPv4Broadcast, fakeNetNumber, err)
+	if r.LocalAddress() != nic2Addr.Address {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), nic2Addr.Address)
+	}
+
+	if r.RemoteAddress() != header.IPv4Broadcast {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), header.IPv4Broadcast)
 	}
 
 	// 2. Case: Having an explicit route for broadcast will select that one.
@@ -1732,11 +1719,12 @@
 	if err != nil {
 		t.Fatalf("FindRoute(0, \"\", %s, %d) failed: %s", header.IPv4Broadcast, fakeNetNumber, err)
 	}
-	wantRoute = stack.Route{}
-	wantRoute.LocalAddress = nic1Addr.Address
-	wantRoute.RemoteAddress = header.IPv4Broadcast
-	if err := verifyRoute(r, &wantRoute); err != nil {
-		t.Errorf("FindRoute(0, \"\", %s, %d) returned unexpected Route: %s)", header.IPv4Broadcast, fakeNetNumber, err)
+	if r.LocalAddress() != nic1Addr.Address {
+		t.Errorf("got Route.LocalAddress() = %s, want = %s", r.LocalAddress(), nic1Addr.Address)
+	}
+
+	if r.RemoteAddress() != header.IPv4Broadcast {
+		t.Errorf("got Route.RemoteAddress() = %s, want = %s", r.RemoteAddress(), header.IPv4Broadcast)
 	}
 }
 
@@ -1817,11 +1805,11 @@
 				if err != nil {
 					t.Fatalf("FindRoute(1, %v, %v, %v) failed: %v", anyAddr, tc.address, fakeNetNumber, err)
 				}
-				if r.LocalAddress != anyAddr {
-					t.Errorf("Bad local address: got %v, want = %v", r.LocalAddress, anyAddr)
+				if r.LocalAddress() != anyAddr {
+					t.Errorf("Bad local address: got %v, want = %v", r.LocalAddress(), anyAddr)
 				}
-				if r.RemoteAddress != tc.address {
-					t.Errorf("Bad remote address: got %v, want = %v", r.RemoteAddress, tc.address)
+				if r.RemoteAddress() != tc.address {
+					t.Errorf("Bad remote address: got %v, want = %v", r.RemoteAddress(), tc.address)
 				}
 			}
 			// If the NIC doesn't exist, it won't work.
@@ -3654,23 +3642,23 @@
 			if err != nil {
 				t.Fatalf("FindRoute(%d, '', %s, %d): %s", unspecifiedNICID, test.remoteAddr, netProto, err)
 			}
-			if r.LocalAddress != test.expectedLocalAddress {
-				t.Errorf("got r.LocalAddress = %s, want = %s", r.LocalAddress, test.expectedLocalAddress)
+			if r.LocalAddress() != test.expectedLocalAddress {
+				t.Errorf("got r.LocalAddress() = %s, want = %s", r.LocalAddress(), test.expectedLocalAddress)
 			}
-			if r.RemoteAddress != test.expectedRemoteAddress {
-				t.Errorf("got r.RemoteAddress = %s, want = %s", r.RemoteAddress, test.expectedRemoteAddress)
+			if r.RemoteAddress() != test.expectedRemoteAddress {
+				t.Errorf("got r.RemoteAddress = %s, want = %s", r.RemoteAddress(), test.expectedRemoteAddress)
 			}
 			if got := r.RemoteLinkAddress(); got != test.expectedRemoteLinkAddress {
 				t.Errorf("got r.RemoteLinkAddress() = %s, want = %s", got, test.expectedRemoteLinkAddress)
 			}
-			if r.NextHop != test.expectedNextHop {
-				t.Errorf("got r.NextHop = %s, want = %s", r.NextHop, test.expectedNextHop)
+			if r.NextHop() != test.expectedNextHop {
+				t.Errorf("got r.NextHop() = %s, want = %s", r.NextHop(), test.expectedNextHop)
 			}
-			if r.NetProto != test.expectedNetProto {
-				t.Errorf("got r.NetProto = %d, want = %d", r.NetProto, test.expectedNetProto)
+			if r.NetProto() != test.expectedNetProto {
+				t.Errorf("got r.NetProto() = %d, want = %d", r.NetProto(), test.expectedNetProto)
 			}
-			if r.Loop != test.expectedLoop {
-				t.Errorf("got r.Loop = %x, want = %x", r.Loop, test.expectedLoop)
+			if r.Loop() != test.expectedLoop {
+				t.Errorf("got r.Loop() = %x, want = %x", r.Loop(), test.expectedLoop)
 			}
 		})
 	}
@@ -4245,11 +4233,11 @@
 				return
 			}
 
-			if r.LocalAddress != test.localAddr {
-				t.Errorf("got r.LocalAddress = %s, want = %s", r.LocalAddress, test.localAddr)
+			if r.LocalAddress() != test.localAddr {
+				t.Errorf("got r.LocalAddress() = %s, want = %s", r.LocalAddress(), test.localAddr)
 			}
-			if r.RemoteAddress != test.netCfg.remoteAddr {
-				t.Errorf("got r.RemoteAddress = %s, want = %s", r.RemoteAddress, test.netCfg.remoteAddr)
+			if r.RemoteAddress() != test.netCfg.remoteAddr {
+				t.Errorf("got r.RemoteAddress() = %s, want = %s", r.RemoteAddress(), test.netCfg.remoteAddr)
 			}
 
 			if t.Failed() {
diff --git a/pkg/tcpip/stack/transport_test.go b/pkg/tcpip/stack/transport_test.go
index bebf4e6..054cced 100644
--- a/pkg/tcpip/stack/transport_test.go
+++ b/pkg/tcpip/stack/transport_test.go
@@ -92,7 +92,7 @@
 }
 
 func (f *fakeTransportEndpoint) Write(p tcpip.Payloader, opts tcpip.WriteOptions) (int64, tcpip.Error) {
-	if len(f.route.RemoteAddress) == 0 {
+	if len(f.route.RemoteAddress()) == 0 {
 		return 0, &tcpip.ErrNoRoute{}
 	}
 
@@ -230,7 +230,7 @@
 			NetProto: f.NetProto,
 		},
 		proto:    f.proto,
-		peerAddr: route.RemoteAddress,
+		peerAddr: route.RemoteAddress(),
 		route:    route,
 	}
 	ep.ops.InitHandler(ep, f.proto.stack, tcpip.GetStackSendBufferLimits)
diff --git a/pkg/tcpip/transport/icmp/endpoint.go b/pkg/tcpip/transport/icmp/endpoint.go
index 06c63e7..1dce35c 100644
--- a/pkg/tcpip/transport/icmp/endpoint.go
+++ b/pkg/tcpip/transport/icmp/endpoint.go
@@ -467,8 +467,8 @@
 	dataRange := pkt.Data().AsRange()
 	icmpv6.SetChecksum(header.ICMPv6Checksum(header.ICMPv6ChecksumParams{
 		Header:      icmpv6,
-		Src:         r.LocalAddress,
-		Dst:         r.RemoteAddress,
+		Src:         r.LocalAddress(),
+		Dst:         r.RemoteAddress(),
 		PayloadCsum: dataRange.Checksum(),
 		PayloadLen:  dataRange.Size(),
 	}))
@@ -536,9 +536,9 @@
 	}
 
 	id := stack.TransportEndpointID{
-		LocalAddress:  r.LocalAddress,
+		LocalAddress:  r.LocalAddress(),
 		LocalPort:     localPort,
-		RemoteAddress: r.RemoteAddress,
+		RemoteAddress: r.RemoteAddress(),
 	}
 
 	// Even if we're connected, this endpoint can still be used to send
diff --git a/pkg/tcpip/transport/icmp/endpoint_state.go b/pkg/tcpip/transport/icmp/endpoint_state.go
index c9fa997..a3c6db5 100644
--- a/pkg/tcpip/transport/icmp/endpoint_state.go
+++ b/pkg/tcpip/transport/icmp/endpoint_state.go
@@ -82,7 +82,7 @@
 			panic(err)
 		}
 
-		e.ID.LocalAddress = e.route.LocalAddress
+		e.ID.LocalAddress = e.route.LocalAddress()
 	} else if len(e.ID.LocalAddress) != 0 { // stateBound
 		if e.stack.CheckLocalAddress(e.RegisterNICID, e.NetProto, e.ID.LocalAddress) == 0 {
 			panic(&tcpip.ErrBadLocalAddress{})
diff --git a/pkg/tcpip/transport/raw/endpoint.go b/pkg/tcpip/transport/raw/endpoint.go
index 2709be9..4b2f083 100644
--- a/pkg/tcpip/transport/raw/endpoint.go
+++ b/pkg/tcpip/transport/raw/endpoint.go
@@ -614,7 +614,7 @@
 
 	// If connected, only accept packets from the remote address we
 	// connected to.
-	if e.connected && e.route.RemoteAddress != remoteAddr {
+	if e.connected && e.route.RemoteAddress() != remoteAddr {
 		e.rcvMu.Unlock()
 		e.mu.RUnlock()
 		return
diff --git a/pkg/tcpip/transport/tcp/connect.go b/pkg/tcpip/transport/tcp/connect.go
index 3404af6..b32fe2f 100644
--- a/pkg/tcpip/transport/tcp/connect.go
+++ b/pkg/tcpip/transport/tcp/connect.go
@@ -811,7 +811,7 @@
 		tf.rcvWnd = math.MaxUint16
 	}
 
-	if r.Loop&stack.PacketLoop == 0 && gso != nil && gso.Type == stack.GSOSW && int(gso.MSS) < data.Size() {
+	if r.Loop()&stack.PacketLoop == 0 && gso != nil && gso.Type == stack.GSOSW && int(gso.MSS) < data.Size() {
 		return sendTCPBatch(r, tf, data, gso, owner)
 	}
 
diff --git a/pkg/tcpip/transport/tcp/endpoint.go b/pkg/tcpip/transport/tcp/endpoint.go
index 43d3443..0a5e9cb 100644
--- a/pkg/tcpip/transport/tcp/endpoint.go
+++ b/pkg/tcpip/transport/tcp/endpoint.go
@@ -2211,8 +2211,8 @@
 	defer r.Release()
 
 	netProtos := []tcpip.NetworkProtocolNumber{netProto}
-	e.ID.LocalAddress = r.LocalAddress
-	e.ID.RemoteAddress = r.RemoteAddress
+	e.ID.LocalAddress = r.LocalAddress()
+	e.ID.RemoteAddress = r.RemoteAddress()
 	e.ID.RemotePort = addr.Port
 
 	if e.ID.LocalPort != 0 {
@@ -3102,7 +3102,7 @@
 
 func (e *endpoint) initHardwareGSO() {
 	gso := &stack.GSO{}
-	switch e.route.NetProto {
+	switch e.route.NetProto() {
 	case header.IPv4ProtocolNumber:
 		gso.Type = stack.GSOTCPv4
 		gso.L3HdrLen = header.IPv4MinimumSize
diff --git a/pkg/tcpip/transport/udp/endpoint.go b/pkg/tcpip/transport/udp/endpoint.go
index c0f5664..0f59181 100644
--- a/pkg/tcpip/transport/udp/endpoint.go
+++ b/pkg/tcpip/transport/udp/endpoint.go
@@ -534,11 +534,11 @@
 		if so.GetRecvError() {
 			so.QueueLocalErr(
 				&tcpip.ErrMessageTooLong{},
-				route.NetProto,
+				route.NetProto(),
 				header.UDPMaximumPacketSize,
 				tcpip.FullAddress{
 					NIC:  route.NICID(),
-					Addr: route.RemoteAddress,
+					Addr: route.RemoteAddress(),
 					Port: dstPort,
 				},
 				v,
@@ -550,7 +550,7 @@
 	ttl := e.ttl
 	useDefaultTTL := ttl == 0
 
-	if header.IsV4MulticastAddress(route.RemoteAddress) || header.IsV6MulticastAddress(route.RemoteAddress) {
+	if header.IsV4MulticastAddress(route.RemoteAddress()) || header.IsV6MulticastAddress(route.RemoteAddress()) {
 		ttl = e.multicastTTL
 		// Multicast allows a 0 TTL.
 		useDefaultTTL = false
@@ -861,7 +861,7 @@
 	// transmitter skipped the checksum generation (RFC768).
 	// On IPv6, UDP checksum is not optional (RFC2460 Section 8.1).
 	if r.RequiresTXTransportChecksum() &&
-		(!noChecksum || r.NetProto == header.IPv6ProtocolNumber) {
+		(!noChecksum || r.NetProto() == header.IPv6ProtocolNumber) {
 		xsum := r.PseudoHeaderChecksum(ProtocolNumber, length)
 		for _, v := range data.Views() {
 			xsum = header.Checksum(v, xsum)
@@ -992,11 +992,11 @@
 		LocalAddress:  e.ID.LocalAddress,
 		LocalPort:     localPort,
 		RemotePort:    addr.Port,
-		RemoteAddress: r.RemoteAddress,
+		RemoteAddress: r.RemoteAddress(),
 	}
 
 	if e.EndpointState() == StateInitial {
-		id.LocalAddress = r.LocalAddress
+		id.LocalAddress = r.LocalAddress()
 	}
 
 	// Even if we're connected, this endpoint can still be used to send
@@ -1204,7 +1204,7 @@
 
 	addr := e.ID.LocalAddress
 	if e.EndpointState() == StateConnected {
-		addr = e.route.LocalAddress
+		addr = e.route.LocalAddress()
 	}
 
 	return tcpip.FullAddress{