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