View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2024 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix.darwin.system;
19  
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotEquals;
22  import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  import static org.junit.jupiter.api.Assertions.fail;
25  import static org.junit.jupiter.api.Assumptions.assumeTrue;
26  
27  import java.io.IOException;
28  import java.net.Inet4Address;
29  import java.net.InetAddress;
30  import java.net.SocketException;
31  import java.net.UnknownHostException;
32  import java.nio.ByteBuffer;
33  import java.nio.ByteOrder;
34  import java.time.Duration;
35  import java.util.Objects;
36  import java.util.concurrent.CompletableFuture;
37  import java.util.concurrent.TimeUnit;
38  
39  import org.junit.jupiter.api.Test;
40  import org.newsclub.net.unix.AFSYSTEMSocketAddress;
41  import org.newsclub.net.unix.AFSYSTEMSocketAddress.SysAddr;
42  import org.newsclub.net.unix.AFSocketCapability;
43  import org.newsclub.net.unix.AFSocketCapabilityRequirement;
44  
45  import com.kohlschutter.testutil.ExecutionEnvironmentRequirement;
46  import com.kohlschutter.testutil.ExecutionEnvironmentRequirement.Rule;
47  
48  /**
49   * Demo code to exercise AF_SYSTEM with UTUN_CONTROL.
50   *
51   * Creates a PtP VPN tunnel, sends a ping via Java SDK code, parses the ICMP echo request (ping)
52   * packet, and responds with a hand-crafted ICMP echo reply (pong).
53   *
54   * @author Christian Kohlschütter
55   */
56  @SuppressWarnings("PMD.AvoidUsingHardCodedIP")
57  public class UtunTest {
58    private static final Inet4Address UTUN_SRC_IP;
59    private static final Inet4Address UTUN_DST_IP;
60  
61    static {
62      try {
63        UTUN_SRC_IP = (Inet4Address) InetAddress.getByName("169.254.3.4"); // "this host"
64        UTUN_DST_IP = (Inet4Address) InetAddress.getByName("169.254.3.5"); // "other end"
65      } catch (UnknownHostException e) {
66        throw new IllegalStateException(e);
67      }
68    }
69  
70    /**
71     * Dummy method to indicate the given parameter is not checked by our test code.
72     *
73     * @param v The parameter.
74     * @return The parameter.
75     */
76    private static Object unchecked(Object v) {
77      return v;
78    }
79  
80    /**
81     * Returns the given IPv4 address as an integer.
82     *
83     * @param addr The IPv4 address object.
84     * @return The integer.
85     */
86    private static int getAddressAsInt(Inet4Address addr) {
87      // In the JDK implementation of Inet4Address, this happens to be the hash code.
88      return addr.hashCode();
89    }
90  
91    @SuppressWarnings({
92        "checkstyle:VariableDeclarationUsageDistance", "PMD.JUnitTestContainsTooManyAsserts",
93        "PMD.AvoidBranchingStatementAsLastInLoop"})
94    @Test
95    @ExecutionEnvironmentRequirement(root = Rule.REQUIRED)
96    @AFSocketCapabilityRequirement(AFSocketCapability.CAPABILITY_DARWIN)
97    public void testTunnelPingPong() throws Exception {
98      try (AFSYSTEMDatagramSocket socket = AFSYSTEMDatagramSocket.newInstance()) {
99        int id = socket.getNodeIdentity(WellKnownKernelControlNames.UTUN_CONTROL);
100 
101       // NOTE: Connecting requires root privileges, but we could do that in a separate process
102       // and send the socket FD via AF_UNIX to a non-privileged helper process.
103       try {
104         socket.connect(AFSYSTEMSocketAddress.ofSysAddrIdUnit(SysAddr.AF_SYS_CONTROL, id, 0));
105       } catch (SocketException e) {
106         assumeTrue(false, "Could not connect to UTUN_CONTROL: " + e);
107         return;
108       }
109 
110       assertTimeoutPreemptively(Duration.ofSeconds(5), () -> {
111 
112         AFSYSTEMSocketAddress rsa = socket.getRemoteSocketAddress();
113         Objects.requireNonNull(rsa);
114 
115         assertEquals(SysAddr.AF_SYS_CONTROL, rsa.getSysAddr());
116         assertEquals(id, rsa.getId());
117         assertNotEquals(0, rsa.getUnit()); // utunN: N=(unit-1), e.g., unit=9 -> utun8
118 
119         String utun = "utun" + (rsa.getUnit() - 1);
120         // System.out.println(utun);
121 
122         Process p = Runtime.getRuntime().exec(new String[] {
123             "/sbin/ifconfig", utun, UTUN_SRC_IP.getHostAddress(), UTUN_DST_IP.getHostAddress()});
124         int rcIfconfig;
125         try {
126           rcIfconfig = p.waitFor();
127         } finally {
128           p.destroyForcibly();
129         }
130 
131         assertEquals(0, rcIfconfig, "Could not set IP address for " + utun);
132 
133         AFSYSTEMDatagramChannel channel = socket.getChannel();
134         ByteBuffer bb = ByteBuffer.allocateDirect(1500).order(ByteOrder.BIG_ENDIAN);
135 
136         CompletableFuture<Boolean> ping = CompletableFuture.supplyAsync(() -> {
137           try {
138             return UTUN_DST_IP.isReachable(1000);
139           } catch (IOException e) {
140             e.printStackTrace();
141             return false;
142           }
143         });
144 
145         while (channel.read(bb) >= 0) {
146           bb.flip();
147 
148           // Request: Domain (AF_INET) + IPv4 header + ICMP header + ICMP payload
149 
150           int totalSize = bb.remaining();
151           // assertEquals(76, totalSize); // 4 byte domain header + 72 bytes packet length
152 
153           int domain = bb.getInt();
154           assertEquals(IPUtil.DOMAIN_AF_INET, domain, "Expect domain 2 (AF_INET)");
155 
156           int ipHeaderStartPos = bb.position();
157 
158           int versionAndIHL = bb.get() & 0xFF;
159           int version = versionAndIHL >> 4;
160           assertEquals(4, version, "expect IPv4 packet");
161 
162           // see https://en.wikipedia.org/wiki/Internet_Protocol_version_4#Header
163 
164           int ihl = versionAndIHL & 0b1111;
165           int ihlBytes = ihl * 32 /* bit */ / 8;
166           assertTrue(ihlBytes >= 20, "expect (at least) 20 bytes header length");
167 
168           int tosDSCP = (bb.get() & 0xFF);
169           unchecked(tosDSCP);
170 
171           int totalLen = (bb.getShort() & 0xFFFF);
172           assertEquals(totalSize - 4, totalLen);
173 
174           int identification = (bb.getShort() & 0xFFFF);
175           unchecked(identification);
176 
177           int flagsAndFragmentOffset = (bb.getShort() & 0xFFFF);
178           int flags = flagsAndFragmentOffset >> 13;
179           int fragmentOffset = flagsAndFragmentOffset & 0b1_1111_1111_1111;
180           assertEquals(0, flags);
181           assertEquals(0, fragmentOffset);
182 
183           int ttl = bb.get() & 0xFF;
184           assertNotEquals(0, ttl); // e.g., 65
185 
186           int protocol = bb.get() & 0xFF;
187           assertEquals(IPUtil.AF_INET_PROTOCOL_ICMP, protocol); // 1 == ICMP
188 
189           int headerChecksum = bb.getShort() & 0xFFFF;
190           // see below for verification
191 
192           int srcIP = bb.getInt();
193           int dstIP = bb.getInt();
194 
195           assertEquals(getAddressAsInt(UTUN_SRC_IP), srcIP); // 10.250.3.4
196           assertEquals(getAddressAsInt(UTUN_DST_IP), dstIP); // 10.250.3.5
197 
198           // when ihl=5 -> ihlBytes=ihl*4=20, there are no more options
199           // but let's check nevertheless...
200 
201           int remainingHeaderLength = ihlBytes - 20;
202           if (remainingHeaderLength > 0) {
203             System.err.println("Warning: Found unexpected Options section in IPv4 header; len="
204                 + remainingHeaderLength);
205             bb.position(bb.position() + remainingHeaderLength);
206           }
207 
208           // we're at the end of the IPv4 header
209 
210           int computedHeaderChecksum = IPUtil.checksumIPv4header(bb, ipHeaderStartPos, bb
211               .position());
212           assertEquals(computedHeaderChecksum, headerChecksum);
213 
214           int icmpSize = bb.remaining();
215           // assertEquals(52, icmpSize); // ICMP header + optional data
216 
217           int icmpBeginPosition = bb.position();
218 
219           // begin ICMP header
220           int icmpType = bb.get() & 0xFF;
221           assertEquals(8, icmpType); // 8 = Echo Request
222 
223           int icmpCode = bb.get() & 0xFF;
224           assertEquals(0, icmpCode); // Echo Request has no other Code
225 
226           int icmpChecksum = bb.getShort() & 0xFFFF; // checked below
227 
228           int icmpEchoIdentifier = bb.getShort() & 0xFFFF;
229           int icmpEchoSequenceNumber = bb.getShort() & 0xFFFF;
230 
231           unchecked(icmpEchoIdentifier);
232           assertEquals(1, icmpEchoSequenceNumber); // first echo packet
233 
234           int icmpChecksumComputed = //
235               IPUtil.checksumICMPheader(bb, icmpBeginPosition, bb.position() + bb.remaining());
236           assertEquals(icmpChecksumComputed, icmpChecksum);
237 
238           // Now it's time to craft an echo response
239           // Response: "AF_INET" + IPv4 header + ICMP header + ICMP payload from echo request
240 
241           ByteBuffer response = ByteBuffer.allocate(IPUtil.DOMAIN_HEADER_LENGTH
242               + IPUtil.IPV4_DEFAULT_HEADER_SIZE + icmpSize).order(ByteOrder.BIG_ENDIAN);
243           response.putInt(IPUtil.DOMAIN_AF_INET);
244           IPUtil.putIPv4Header(response, icmpSize, IPUtil.AF_INET_PROTOCOL_ICMP, dstIP, srcIP);
245 
246           int responsePayloadStart = response.position();
247           IPUtil.checksumIPv4header(response, IPUtil.DOMAIN_HEADER_LENGTH, responsePayloadStart);
248 
249           IPUtil.putICMPEchoResponse(response, (short) icmpEchoIdentifier,
250               (short) icmpEchoSequenceNumber, bb);
251           assertEquals(0, bb.remaining()); // writeEchoResponse consumed the payload
252           int responsePayloadEnd = response.position();
253 
254           IPUtil.checksumICMPheader(response, responsePayloadStart, responsePayloadEnd);
255 
256           response.flip();
257           int written = channel.write(response);
258           bb.clear();
259 
260           assertEquals(response.capacity(), written);
261 
262           assertTrue(ping.get(1, TimeUnit.SECONDS));
263 
264           return;
265         }
266 
267         fail("Nothing received");
268       });
269     }
270   }
271 }