- * This implementation was imported to WireGuard from noise-java:
- * https://github.com/rweather/noise-java
- *
- * This implementation is based on that from arduinolibs:
- * https://github.com/rweather/arduinolibs
- *
- * Differences in this version are due to using 26-bit limbs for the
- * representation instead of the 8/16/32-bit limbs in the original.
- *
- * References: http://cr.yp.to/ecdh.html, RFC 7748
- */
-@SuppressWarnings({"MagicNumber", "NonConstantFieldWithUpperCaseName", "SuspiciousNameCombination"})
-public final class Curve25519 {
- // Numbers modulo 2^255 - 19 are broken up into ten 26-bit words.
- private static final int NUM_LIMBS_255BIT = 10;
- private static final int NUM_LIMBS_510BIT = 20;
-
- private final int[] A;
- private final int[] AA;
- private final int[] B;
- private final int[] BB;
- private final int[] C;
- private final int[] CB;
- private final int[] D;
- private final int[] DA;
- private final int[] E;
- private final long[] t1;
- private final int[] t2;
- private final int[] x_1;
- private final int[] x_2;
- private final int[] x_3;
- private final int[] z_2;
- private final int[] z_3;
-
- /**
- * Constructs the temporary state holder for Curve25519 evaluation.
- */
- private Curve25519() {
- // Allocate memory for all of the temporary variables we will need.
- x_1 = new int[NUM_LIMBS_255BIT];
- x_2 = new int[NUM_LIMBS_255BIT];
- x_3 = new int[NUM_LIMBS_255BIT];
- z_2 = new int[NUM_LIMBS_255BIT];
- z_3 = new int[NUM_LIMBS_255BIT];
- A = new int[NUM_LIMBS_255BIT];
- B = new int[NUM_LIMBS_255BIT];
- C = new int[NUM_LIMBS_255BIT];
- D = new int[NUM_LIMBS_255BIT];
- E = new int[NUM_LIMBS_255BIT];
- AA = new int[NUM_LIMBS_255BIT];
- BB = new int[NUM_LIMBS_255BIT];
- DA = new int[NUM_LIMBS_255BIT];
- CB = new int[NUM_LIMBS_255BIT];
- t1 = new long[NUM_LIMBS_510BIT];
- t2 = new int[NUM_LIMBS_510BIT];
- }
-
- /**
- * Conditional swap of two values.
- *
- * @param select Set to 1 to swap, 0 to leave as-is.
- * @param x The first value.
- * @param y The second value.
- */
- private static void cswap(int select, final int[] x, final int[] y) {
- select = -select;
- for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
- final int dummy = select & (x[index] ^ y[index]);
- x[index] ^= dummy;
- y[index] ^= dummy;
- }
- }
-
- /**
- * Evaluates the Curve25519 curve.
- *
- * @param result Buffer to place the result of the evaluation into.
- * @param offset Offset into the result buffer.
- * @param privateKey The private key to use in the evaluation.
- * @param publicKey The public key to use in the evaluation, or null
- * if the base point of the curve should be used.
- */
- public static void eval(final byte[] result, final int offset,
- final byte[] privateKey, @Nullable final byte[] publicKey) {
- final Curve25519 state = new Curve25519();
- try {
- // Unpack the public key value. If null, use 9 as the base point.
- Arrays.fill(state.x_1, 0);
- if (publicKey != null) {
- // Convert the input value from little-endian into 26-bit limbs.
- for (int index = 0; index < 32; ++index) {
- final int bit = (index * 8) % 26;
- final int word = (index * 8) / 26;
- final int value = publicKey[index] & 0xFF;
- if (bit <= (26 - 8)) {
- state.x_1[word] |= value << bit;
- } else {
- state.x_1[word] |= value << bit;
- state.x_1[word] &= 0x03FFFFFF;
- state.x_1[word + 1] |= value >> (26 - bit);
- }
- }
-
- // Just in case, we reduce the number modulo 2^255 - 19 to
- // make sure that it is in range of the field before we start.
- // This eliminates values between 2^255 - 19 and 2^256 - 1.
- state.reduceQuick(state.x_1);
- state.reduceQuick(state.x_1);
- } else {
- state.x_1[0] = 9;
- }
-
- // Initialize the other temporary variables.
- Arrays.fill(state.x_2, 0); // x_2 = 1
- state.x_2[0] = 1;
- Arrays.fill(state.z_2, 0); // z_2 = 0
- System.arraycopy(state.x_1, 0, state.x_3, 0, state.x_1.length); // x_3 = x_1
- Arrays.fill(state.z_3, 0); // z_3 = 1
- state.z_3[0] = 1;
-
- // Evaluate the curve for every bit of the private key.
- state.evalCurve(privateKey);
-
- // Compute x_2 * (z_2 ^ (p - 2)) where p = 2^255 - 19.
- state.recip(state.z_3, state.z_2);
- state.mul(state.x_2, state.x_2, state.z_3);
-
- // Convert x_2 into little-endian in the result buffer.
- for (int index = 0; index < 32; ++index) {
- final int bit = (index * 8) % 26;
- final int word = (index * 8) / 26;
- if (bit <= (26 - 8))
- result[offset + index] = (byte) (state.x_2[word] >> bit);
- else
- result[offset + index] = (byte) ((state.x_2[word] >> bit) | (state.x_2[word + 1] << (26 - bit)));
- }
- } finally {
- // Clean up all temporary state before we exit.
- state.destroy();
- }
- }
-
- /**
- * Subtracts two numbers modulo 2^255 - 19.
- *
- * @param result The result.
- * @param x The first number to subtract.
- * @param y The second number to subtract.
- */
- private static void sub(final int[] result, final int[] x, final int[] y) {
- int index;
- int borrow;
-
- // Subtract y from x to generate the intermediate result.
- borrow = 0;
- for (index = 0; index < NUM_LIMBS_255BIT; ++index) {
- borrow = x[index] - y[index] - ((borrow >> 26) & 0x01);
- result[index] = borrow & 0x03FFFFFF;
- }
-
- // If we had a borrow, then the result has gone negative and we
- // have to add 2^255 - 19 to the result to make it positive again.
- // The top bits of "borrow" will be all 1's if there is a borrow
- // or it will be all 0's if there was no borrow. Easiest is to
- // conditionally subtract 19 and then mask off the high bits.
- borrow = result[0] - ((-((borrow >> 26) & 0x01)) & 19);
- result[0] = borrow & 0x03FFFFFF;
- for (index = 1; index < NUM_LIMBS_255BIT; ++index) {
- borrow = result[index] - ((borrow >> 26) & 0x01);
- result[index] = borrow & 0x03FFFFFF;
- }
- result[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
- }
-
- /**
- * Adds two numbers modulo 2^255 - 19.
- *
- * @param result The result.
- * @param x The first number to add.
- * @param y The second number to add.
- */
- private void add(final int[] result, final int[] x, final int[] y) {
- int carry = x[0] + y[0];
- result[0] = carry & 0x03FFFFFF;
- for (int index = 1; index < NUM_LIMBS_255BIT; ++index) {
- carry = (carry >> 26) + x[index] + y[index];
- result[index] = carry & 0x03FFFFFF;
- }
- reduceQuick(result);
- }
-
- /**
- * Destroy all sensitive data in this object.
- */
- private void destroy() {
- // Destroy all temporary variables.
- Arrays.fill(x_1, 0);
- Arrays.fill(x_2, 0);
- Arrays.fill(x_3, 0);
- Arrays.fill(z_2, 0);
- Arrays.fill(z_3, 0);
- Arrays.fill(A, 0);
- Arrays.fill(B, 0);
- Arrays.fill(C, 0);
- Arrays.fill(D, 0);
- Arrays.fill(E, 0);
- Arrays.fill(AA, 0);
- Arrays.fill(BB, 0);
- Arrays.fill(DA, 0);
- Arrays.fill(CB, 0);
- Arrays.fill(t1, 0L);
- Arrays.fill(t2, 0);
- }
-
- /**
- * Evaluates the curve for every bit in a secret key.
- *
- * @param s The 32-byte secret key.
- */
- private void evalCurve(final byte[] s) {
- int sposn = 31;
- int sbit = 6;
- int svalue = s[sposn] | 0x40;
- int swap = 0;
-
- // Iterate over all 255 bits of "s" from the highest to the lowest.
- // We ignore the high bit of the 256-bit representation of "s".
- while (true) {
- // Conditional swaps on entry to this bit but only if we
- // didn't swap on the previous bit.
- final int select = (svalue >> sbit) & 0x01;
- swap ^= select;
- cswap(swap, x_2, x_3);
- cswap(swap, z_2, z_3);
- swap = select;
-
- // Evaluate the curve.
- add(A, x_2, z_2); // A = x_2 + z_2
- square(AA, A); // AA = A^2
- sub(B, x_2, z_2); // B = x_2 - z_2
- square(BB, B); // BB = B^2
- sub(E, AA, BB); // E = AA - BB
- add(C, x_3, z_3); // C = x_3 + z_3
- sub(D, x_3, z_3); // D = x_3 - z_3
- mul(DA, D, A); // DA = D * A
- mul(CB, C, B); // CB = C * B
- add(x_3, DA, CB); // x_3 = (DA + CB)^2
- square(x_3, x_3);
- sub(z_3, DA, CB); // z_3 = x_1 * (DA - CB)^2
- square(z_3, z_3);
- mul(z_3, z_3, x_1);
- mul(x_2, AA, BB); // x_2 = AA * BB
- mulA24(z_2, E); // z_2 = E * (AA + a24 * E)
- add(z_2, z_2, AA);
- mul(z_2, z_2, E);
-
- // Move onto the next lower bit of "s".
- if (sbit > 0) {
- --sbit;
- } else if (sposn == 0) {
- break;
- } else if (sposn == 1) {
- --sposn;
- svalue = s[sposn] & 0xF8;
- sbit = 7;
- } else {
- --sposn;
- svalue = s[sposn];
- sbit = 7;
- }
- }
-
- // Final conditional swaps.
- cswap(swap, x_2, x_3);
- cswap(swap, z_2, z_3);
- }
-
- /**
- * Multiplies two numbers modulo 2^255 - 19.
- *
- * @param result The result.
- * @param x The first number to multiply.
- * @param y The second number to multiply.
- */
- private void mul(final int[] result, final int[] x, final int[] y) {
- // Multiply the two numbers to create the intermediate result.
- long v = x[0];
- for (int i = 0; i < NUM_LIMBS_255BIT; ++i) {
- t1[i] = v * y[i];
- }
- for (int i = 1; i < NUM_LIMBS_255BIT; ++i) {
- v = x[i];
- for (int j = 0; j < (NUM_LIMBS_255BIT - 1); ++j) {
- t1[i + j] += v * y[j];
- }
- t1[i + NUM_LIMBS_255BIT - 1] = v * y[NUM_LIMBS_255BIT - 1];
- }
-
- // Propagate carries and convert back into 26-bit words.
- v = t1[0];
- t2[0] = ((int) v) & 0x03FFFFFF;
- for (int i = 1; i < NUM_LIMBS_510BIT; ++i) {
- v = (v >> 26) + t1[i];
- t2[i] = ((int) v) & 0x03FFFFFF;
- }
-
- // Reduce the result modulo 2^255 - 19.
- reduce(result, t2, NUM_LIMBS_255BIT);
- }
-
- /**
- * Multiplies a number by the a24 constant, modulo 2^255 - 19.
- *
- * @param result The result.
- * @param x The number to multiply by a24.
- */
- private void mulA24(final int[] result, final int[] x) {
- final long a24 = 121665;
- long carry = 0;
- for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
- carry += a24 * x[index];
- t2[index] = ((int) carry) & 0x03FFFFFF;
- carry >>= 26;
- }
- t2[NUM_LIMBS_255BIT] = ((int) carry) & 0x03FFFFFF;
- reduce(result, t2, 1);
- }
-
- /**
- * Raise x to the power of (2^250 - 1).
- *
- * @param result The result. Must not overlap with x.
- * @param x The argument.
- */
- private void pow250(final int[] result, final int[] x) {
- // The big-endian hexadecimal expansion of (2^250 - 1) is:
- // 03FFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF
- //
- // The naive implementation needs to do 2 multiplications per 1 bit and
- // 1 multiplication per 0 bit. We can improve upon this by creating a
- // pattern 0000000001 ... 0000000001. If we square and multiply the
- // pattern by itself we can turn the pattern into the partial results
- // 0000000011 ... 0000000011, 0000000111 ... 0000000111, etc.
- // This averages out to about 1.1 multiplications per 1 bit instead of 2.
-
- // Build a pattern of 250 bits in length of repeated copies of 0000000001.
- square(A, x);
- for (int j = 0; j < 9; ++j)
- square(A, A);
- mul(result, A, x);
- for (int i = 0; i < 23; ++i) {
- for (int j = 0; j < 10; ++j)
- square(A, A);
- mul(result, result, A);
- }
-
- // Multiply bit-shifted versions of the 0000000001 pattern into
- // the result to "fill in" the gaps in the pattern.
- square(A, result);
- mul(result, result, A);
- for (int j = 0; j < 8; ++j) {
- square(A, A);
- mul(result, result, A);
- }
- }
-
- /**
- * Computes the reciprocal of a number modulo 2^255 - 19.
- *
- * @param result The result. Must not overlap with x.
- * @param x The argument.
- */
- private void recip(final int[] result, final int[] x) {
- // The reciprocal is the same as x ^ (p - 2) where p = 2^255 - 19.
- // The big-endian hexadecimal expansion of (p - 2) is:
- // 7FFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFFF FFFFFFEB
- // Start with the 250 upper bits of the expansion of (p - 2).
- pow250(result, x);
-
- // Deal with the 5 lowest bits of (p - 2), 01011, from highest to lowest.
- square(result, result);
- square(result, result);
- mul(result, result, x);
- square(result, result);
- square(result, result);
- mul(result, result, x);
- square(result, result);
- mul(result, result, x);
- }
-
- /**
- * Reduce a number modulo 2^255 - 19.
- *
- * @param result The result.
- * @param x The value to be reduced. This array will be
- * modified during the reduction.
- * @param size The number of limbs in the high order half of x.
- */
- private void reduce(final int[] result, final int[] x, final int size) {
- // Calculate (x mod 2^255) + ((x / 2^255) * 19) which will
- // either produce the answer we want or it will produce a
- // value of the form "answer + j * (2^255 - 19)". There are
- // 5 left-over bits in the top-most limb of the bottom half.
- int carry = 0;
- int limb = x[NUM_LIMBS_255BIT - 1] >> 21;
- x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
- for (int index = 0; index < size; ++index) {
- limb += x[NUM_LIMBS_255BIT + index] << 5;
- carry += (limb & 0x03FFFFFF) * 19 + x[index];
- x[index] = carry & 0x03FFFFFF;
- limb >>= 26;
- carry >>= 26;
- }
- if (size < NUM_LIMBS_255BIT) {
- // The high order half of the number is short; e.g. for mulA24().
- // Propagate the carry through the rest of the low order part.
- for (int index = size; index < NUM_LIMBS_255BIT; ++index) {
- carry += x[index];
- x[index] = carry & 0x03FFFFFF;
- carry >>= 26;
- }
- }
-
- // The "j" value may still be too large due to the final carry-out.
- // We must repeat the reduction. If we already have the answer,
- // then this won't do any harm but we must still do the calculation
- // to preserve the overall timing. The "j" value will be between
- // 0 and 19, which means that the carry we care about is in the
- // top 5 bits of the highest limb of the bottom half.
- carry = (x[NUM_LIMBS_255BIT - 1] >> 21) * 19;
- x[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
- for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
- carry += x[index];
- result[index] = carry & 0x03FFFFFF;
- carry >>= 26;
- }
-
- // At this point "x" will either be the answer or it will be the
- // answer plus (2^255 - 19). Perform a trial subtraction to
- // complete the reduction process.
- reduceQuick(result);
- }
-
- /**
- * Reduces a number modulo 2^255 - 19 where it is known that the
- * number can be reduced with only 1 trial subtraction.
- *
- * @param x The number to reduce, and the result.
- */
- private void reduceQuick(final int[] x) {
- // Perform a trial subtraction of (2^255 - 19) from "x" which is
- // equivalent to adding 19 and subtracting 2^255. We add 19 here;
- // the subtraction of 2^255 occurs in the next step.
- int carry = 19;
- for (int index = 0; index < NUM_LIMBS_255BIT; ++index) {
- carry += x[index];
- t2[index] = carry & 0x03FFFFFF;
- carry >>= 26;
- }
-
- // If there was a borrow, then the original "x" is the correct answer.
- // If there was no borrow, then "t2" is the correct answer. Select the
- // correct answer but do it in a way that instruction timing will not
- // reveal which value was selected. Borrow will occur if bit 21 of
- // "t2" is zero. Turn the bit into a selection mask.
- final int mask = -((t2[NUM_LIMBS_255BIT - 1] >> 21) & 0x01);
- final int nmask = ~mask;
- t2[NUM_LIMBS_255BIT - 1] &= 0x001FFFFF;
- for (int index = 0; index < NUM_LIMBS_255BIT; ++index)
- x[index] = (x[index] & nmask) | (t2[index] & mask);
- }
-
- /**
- * Squares a number modulo 2^255 - 19.
- *
- * @param result The result.
- * @param x The number to square.
- */
- private void square(final int[] result, final int[] x) {
- mul(result, x, x);
- }
-}
diff --git a/app/src/main/java/com/wireguard/crypto/Ed25519.java b/app/src/main/java/com/wireguard/crypto/Ed25519.java
deleted file mode 100644
index a60babfbb..000000000
--- a/app/src/main/java/com/wireguard/crypto/Ed25519.java
+++ /dev/null
@@ -1,2508 +0,0 @@
-/*
- * Copyright © 2020 WireGuard LLC. All Rights Reserved.
- * Copyright 2017 Google Inc.
- *
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.crypto;
-
-import java.math.BigInteger;
-import java.security.GeneralSecurityException;
-import java.security.MessageDigest;
-import java.util.Arrays;
-
-/**
- * Implementation of Ed25519 signature verification.
- *
- *
This implementation is based on the ed25519/ref10 implementation in NaCl.
- *
- * It implements this twisted Edwards curve:
- *
- *
- * -x^2 + y^2 = 1 + (-121665 / 121666 mod 2^255-19)*x^2*y^2
- *
- *
- * @see Bernstein D.J., Birkner P., Joye M., Lange
- * T., Peters C. (2008) Twisted Edwards Curves
- * @see Hisil H., Wong K.KH., Carter G., Dawson E.
- * (2008) Twisted Edwards Curves Revisited
- */
-public final class Ed25519 {
-
- // d = -121665 / 121666 mod 2^255-19
- private static final long[] D;
- // 2d
- private static final long[] D2;
- // 2^((p-1)/4) mod p where p = 2^255-19
- private static final long[] SQRTM1;
-
- /**
- * Base point for the Edwards twisted curve = (x, 4/5) and its exponentiations. B_TABLE[i][j] =
- * (j+1)*256^i*B for i in [0, 32) and j in [0, 8). Base point B = B_TABLE[0][0]
- */
- private static final CachedXYT[][] B_TABLE;
- private static final CachedXYT[] B2;
-
- private static final BigInteger P_BI =
- BigInteger.valueOf(2).pow(255).subtract(BigInteger.valueOf(19));
- private static final BigInteger D_BI =
- BigInteger.valueOf(-121665).multiply(BigInteger.valueOf(121666).modInverse(P_BI)).mod(P_BI);
- private static final BigInteger D2_BI = BigInteger.valueOf(2).multiply(D_BI).mod(P_BI);
- private static final BigInteger SQRTM1_BI =
- BigInteger.valueOf(2).modPow(P_BI.subtract(BigInteger.ONE).divide(BigInteger.valueOf(4)), P_BI);
-
- private Ed25519() {
- }
-
- private static class Point {
- private BigInteger x;
- private BigInteger y;
- }
-
- private static BigInteger recoverX(BigInteger y) {
- // x^2 = (y^2 - 1) / (d * y^2 + 1) mod 2^255-19
- BigInteger xx =
- y.pow(2)
- .subtract(BigInteger.ONE)
- .multiply(D_BI.multiply(y.pow(2)).add(BigInteger.ONE).modInverse(P_BI));
- BigInteger x = xx.modPow(P_BI.add(BigInteger.valueOf(3)).divide(BigInteger.valueOf(8)), P_BI);
- if (!x.pow(2).subtract(xx).mod(P_BI).equals(BigInteger.ZERO)) {
- x = x.multiply(SQRTM1_BI).mod(P_BI);
- }
- if (x.testBit(0)) {
- x = P_BI.subtract(x);
- }
- return x;
- }
-
- private static Point edwards(Point a, Point b) {
- Point o = new Point();
- BigInteger xxyy = D_BI.multiply(a.x.multiply(b.x).multiply(a.y).multiply(b.y)).mod(P_BI);
- o.x =
- (a.x.multiply(b.y).add(b.x.multiply(a.y)))
- .multiply(BigInteger.ONE.add(xxyy).modInverse(P_BI))
- .mod(P_BI);
- o.y =
- (a.y.multiply(b.y).add(a.x.multiply(b.x)))
- .multiply(BigInteger.ONE.subtract(xxyy).modInverse(P_BI))
- .mod(P_BI);
- return o;
- }
-
- private static byte[] toLittleEndian(BigInteger n) {
- byte[] b = new byte[32];
- byte[] nBytes = n.toByteArray();
- System.arraycopy(nBytes, 0, b, 32 - nBytes.length, nBytes.length);
- for (int i = 0; i < b.length / 2; i++) {
- byte t = b[i];
- b[i] = b[b.length - i - 1];
- b[b.length - i - 1] = t;
- }
- return b;
- }
-
- private static CachedXYT getCachedXYT(Point p) {
- return new CachedXYT(
- Field25519.expand(toLittleEndian(p.y.add(p.x).mod(P_BI))),
- Field25519.expand(toLittleEndian(p.y.subtract(p.x).mod(P_BI))),
- Field25519.expand(toLittleEndian(D2_BI.multiply(p.x).multiply(p.y).mod(P_BI))));
- }
-
- static {
- Point b = new Point();
- b.y = BigInteger.valueOf(4).multiply(BigInteger.valueOf(5).modInverse(P_BI)).mod(P_BI);
- b.x = recoverX(b.y);
-
- D = Field25519.expand(toLittleEndian(D_BI));
- D2 = Field25519.expand(toLittleEndian(D2_BI));
- SQRTM1 = Field25519.expand(toLittleEndian(SQRTM1_BI));
-
- Point bi = b;
- B_TABLE = new CachedXYT[32][8];
- for (int i = 0; i < 32; i++) {
- Point bij = bi;
- for (int j = 0; j < 8; j++) {
- B_TABLE[i][j] = getCachedXYT(bij);
- bij = edwards(bij, bi);
- }
- for (int j = 0; j < 8; j++) {
- bi = edwards(bi, bi);
- }
- }
- bi = b;
- Point b2 = edwards(b, b);
- B2 = new CachedXYT[8];
- for (int i = 0; i < 8; i++) {
- B2[i] = getCachedXYT(bi);
- bi = edwards(bi, b2);
- }
- }
-
- private static final int PUBLIC_KEY_LEN = Field25519.FIELD_LEN;
- private static final int SIGNATURE_LEN = Field25519.FIELD_LEN * 2;
-
- /**
- * Defines field 25519 function based on curve25519-donna C
- * implementation (mostly identical).
- *
- * Field elements are written as an array of signed, 64-bit limbs (an array of longs), least
- * significant first. The value of the field element is:
- *
- *
- * x[0] + 2^26·x[1] + 2^51·x[2] + 2^77·x[3] + 2^102·x[4] + 2^128·x[5] + 2^153·x[6] + 2^179·x[7] +
- * 2^204·x[8] + 2^230·x[9],
- *
- *
- * i.e. the limbs are 26, 25, 26, 25, ... bits wide.
- */
- private static final class Field25519 {
- /**
- * During Field25519 computation, the mixed radix representation may be in different forms:
- *
- * Reduced-size form: the array has size at most 10.
- * Non-reduced-size form: the array is not reduced modulo 2^255 - 19 and has size at most
- * 19.
- *
- *
- * TODO(quannguyen):
- *
- * Clarify ill-defined terminologies.
- * The reduction procedure is different from DJB's paper
- * (http://cr.yp.to/ecdh/curve25519-20060209.pdf). The coefficients after reducing degree and
- * reducing coefficients aren't guaranteed to be in range {-2^25, ..., 2^25}. We should check to
- * see what's going on.
- * Consider using method mult() everywhere and making product() private.
- *
- */
-
- static final int FIELD_LEN = 32;
- static final int LIMB_CNT = 10;
- private static final long TWO_TO_25 = 1 << 25;
- private static final long TWO_TO_26 = TWO_TO_25 << 1;
-
- private static final int[] EXPAND_START = {0, 3, 6, 9, 12, 16, 19, 22, 25, 28};
- private static final int[] EXPAND_SHIFT = {0, 2, 3, 5, 6, 0, 1, 3, 4, 6};
- private static final int[] MASK = {0x3ffffff, 0x1ffffff};
- private static final int[] SHIFT = {26, 25};
-
- /**
- * Sums two numbers: output = in1 + in2
- *
- * On entry: in1, in2 are in reduced-size form.
- */
- static void sum(long[] output, long[] in1, long[] in2) {
- for (int i = 0; i < LIMB_CNT; i++) {
- output[i] = in1[i] + in2[i];
- }
- }
-
- /**
- * Sums two numbers: output += in
- *
- * On entry: in is in reduced-size form.
- */
- static void sum(long[] output, long[] in) {
- sum(output, output, in);
- }
-
- /**
- * Find the difference of two numbers: output = in1 - in2
- * (note the order of the arguments!).
- *
- * On entry: in1, in2 are in reduced-size form.
- */
- static void sub(long[] output, long[] in1, long[] in2) {
- for (int i = 0; i < LIMB_CNT; i++) {
- output[i] = in1[i] - in2[i];
- }
- }
-
- /**
- * Find the difference of two numbers: output = in - output
- * (note the order of the arguments!).
- *
- * On entry: in, output are in reduced-size form.
- */
- static void sub(long[] output, long[] in) {
- sub(output, in, output);
- }
-
- /**
- * Multiply a number by a scalar: output = in * scalar
- */
- static void scalarProduct(long[] output, long[] in, long scalar) {
- for (int i = 0; i < LIMB_CNT; i++) {
- output[i] = in[i] * scalar;
- }
- }
-
- /**
- * Multiply two numbers: out = in2 * in
- *
- * output must be distinct to both inputs. The inputs are reduced coefficient form,
- * the output is not.
- *
- * out[x] <= 14 * the largest product of the input limbs.
- */
- static void product(long[] out, long[] in2, long[] in) {
- out[0] = in2[0] * in[0];
- out[1] = in2[0] * in[1]
- + in2[1] * in[0];
- out[2] = 2 * in2[1] * in[1]
- + in2[0] * in[2]
- + in2[2] * in[0];
- out[3] = in2[1] * in[2]
- + in2[2] * in[1]
- + in2[0] * in[3]
- + in2[3] * in[0];
- out[4] = in2[2] * in[2]
- + 2 * (in2[1] * in[3] + in2[3] * in[1])
- + in2[0] * in[4]
- + in2[4] * in[0];
- out[5] = in2[2] * in[3]
- + in2[3] * in[2]
- + in2[1] * in[4]
- + in2[4] * in[1]
- + in2[0] * in[5]
- + in2[5] * in[0];
- out[6] = 2 * (in2[3] * in[3] + in2[1] * in[5] + in2[5] * in[1])
- + in2[2] * in[4]
- + in2[4] * in[2]
- + in2[0] * in[6]
- + in2[6] * in[0];
- out[7] = in2[3] * in[4]
- + in2[4] * in[3]
- + in2[2] * in[5]
- + in2[5] * in[2]
- + in2[1] * in[6]
- + in2[6] * in[1]
- + in2[0] * in[7]
- + in2[7] * in[0];
- out[8] = in2[4] * in[4]
- + 2 * (in2[3] * in[5] + in2[5] * in[3] + in2[1] * in[7] + in2[7] * in[1])
- + in2[2] * in[6]
- + in2[6] * in[2]
- + in2[0] * in[8]
- + in2[8] * in[0];
- out[9] = in2[4] * in[5]
- + in2[5] * in[4]
- + in2[3] * in[6]
- + in2[6] * in[3]
- + in2[2] * in[7]
- + in2[7] * in[2]
- + in2[1] * in[8]
- + in2[8] * in[1]
- + in2[0] * in[9]
- + in2[9] * in[0];
- out[10] =
- 2 * (in2[5] * in[5] + in2[3] * in[7] + in2[7] * in[3] + in2[1] * in[9] + in2[9] * in[1])
- + in2[4] * in[6]
- + in2[6] * in[4]
- + in2[2] * in[8]
- + in2[8] * in[2];
- out[11] = in2[5] * in[6]
- + in2[6] * in[5]
- + in2[4] * in[7]
- + in2[7] * in[4]
- + in2[3] * in[8]
- + in2[8] * in[3]
- + in2[2] * in[9]
- + in2[9] * in[2];
- out[12] = in2[6] * in[6]
- + 2 * (in2[5] * in[7] + in2[7] * in[5] + in2[3] * in[9] + in2[9] * in[3])
- + in2[4] * in[8]
- + in2[8] * in[4];
- out[13] = in2[6] * in[7]
- + in2[7] * in[6]
- + in2[5] * in[8]
- + in2[8] * in[5]
- + in2[4] * in[9]
- + in2[9] * in[4];
- out[14] = 2 * (in2[7] * in[7] + in2[5] * in[9] + in2[9] * in[5])
- + in2[6] * in[8]
- + in2[8] * in[6];
- out[15] = in2[7] * in[8]
- + in2[8] * in[7]
- + in2[6] * in[9]
- + in2[9] * in[6];
- out[16] = in2[8] * in[8]
- + 2 * (in2[7] * in[9] + in2[9] * in[7]);
- out[17] = in2[8] * in[9]
- + in2[9] * in[8];
- out[18] = 2 * in2[9] * in[9];
- }
-
- /**
- * Reduce a field element by calling reduceSizeByModularReduction and reduceCoefficients.
- *
- * @param input An input array of any length. If the array has 19 elements, it will be used as
- * temporary buffer and its contents changed.
- * @param output An output array of size LIMB_CNT. After the call |output[i]| < 2^26 will hold.
- */
- static void reduce(long[] input, long[] output) {
- long[] tmp;
- if (input.length == 19) {
- tmp = input;
- } else {
- tmp = new long[19];
- System.arraycopy(input, 0, tmp, 0, input.length);
- }
- reduceSizeByModularReduction(tmp);
- reduceCoefficients(tmp);
- System.arraycopy(tmp, 0, output, 0, LIMB_CNT);
- }
-
- /**
- * Reduce a long form to a reduced-size form by taking the input mod 2^255 - 19.
- *
- * On entry: |output[i]| < 14*2^54
- * On exit: |output[0..8]| < 280*2^54
- */
- static void reduceSizeByModularReduction(long[] output) {
- // The coefficients x[10], x[11],..., x[18] are eliminated by reduction modulo 2^255 - 19.
- // For example, the coefficient x[18] is multiplied by 19 and added to the coefficient x[8].
- //
- // Each of these shifts and adds ends up multiplying the value by 19.
- //
- // For output[0..8], the absolute entry value is < 14*2^54 and we add, at most, 19*14*2^54 thus,
- // on exit, |output[0..8]| < 280*2^54.
- output[8] += output[18] << 4;
- output[8] += output[18] << 1;
- output[8] += output[18];
- output[7] += output[17] << 4;
- output[7] += output[17] << 1;
- output[7] += output[17];
- output[6] += output[16] << 4;
- output[6] += output[16] << 1;
- output[6] += output[16];
- output[5] += output[15] << 4;
- output[5] += output[15] << 1;
- output[5] += output[15];
- output[4] += output[14] << 4;
- output[4] += output[14] << 1;
- output[4] += output[14];
- output[3] += output[13] << 4;
- output[3] += output[13] << 1;
- output[3] += output[13];
- output[2] += output[12] << 4;
- output[2] += output[12] << 1;
- output[2] += output[12];
- output[1] += output[11] << 4;
- output[1] += output[11] << 1;
- output[1] += output[11];
- output[0] += output[10] << 4;
- output[0] += output[10] << 1;
- output[0] += output[10];
- }
-
- /**
- * Reduce all coefficients of the short form input so that |x| < 2^26.
- *
- * On entry: |output[i]| < 280*2^54
- */
- static void reduceCoefficients(long[] output) {
- output[10] = 0;
-
- for (int i = 0; i < LIMB_CNT; i += 2) {
- long over = output[i] / TWO_TO_26;
- // The entry condition (that |output[i]| < 280*2^54) means that over is, at most, 280*2^28 in
- // the first iteration of this loop. This is added to the next limb and we can approximate the
- // resulting bound of that limb by 281*2^54.
- output[i] -= over << 26;
- output[i + 1] += over;
-
- // For the first iteration, |output[i+1]| < 281*2^54, thus |over| < 281*2^29. When this is
- // added to the next limb, the resulting bound can be approximated as 281*2^54.
- //
- // For subsequent iterations of the loop, 281*2^54 remains a conservative bound and no
- // overflow occurs.
- over = output[i + 1] / TWO_TO_25;
- output[i + 1] -= over << 25;
- output[i + 2] += over;
- }
- // Now |output[10]| < 281*2^29 and all other coefficients are reduced.
- output[0] += output[10] << 4;
- output[0] += output[10] << 1;
- output[0] += output[10];
-
- output[10] = 0;
- // Now output[1..9] are reduced, and |output[0]| < 2^26 + 19*281*2^29 so |over| will be no more
- // than 2^16.
- long over = output[0] / TWO_TO_26;
- output[0] -= over << 26;
- output[1] += over;
- // Now output[0,2..9] are reduced, and |output[1]| < 2^25 + 2^16 < 2^26. The bound on
- // |output[1]| is sufficient to meet our needs.
- }
-
- /**
- * A helpful wrapper around {@ref Field25519#product}: output = in * in2.
- *
- * On entry: |in[i]| < 2^27 and |in2[i]| < 2^27.
- *
- * The output is reduced degree (indeed, one need only provide storage for 10 limbs) and
- * |output[i]| < 2^26.
- */
- static void mult(long[] output, long[] in, long[] in2) {
- long[] t = new long[19];
- product(t, in, in2);
- // |t[i]| < 2^26
- reduce(t, output);
- }
-
- /**
- * Square a number: out = in**2
- *
- * output must be distinct from the input. The inputs are reduced coefficient form, the output is
- * not.
- *
- * out[x] <= 14 * the largest product of the input limbs.
- */
- private static void squareInner(long[] out, long[] in) {
- out[0] = in[0] * in[0];
- out[1] = 2 * in[0] * in[1];
- out[2] = 2 * (in[1] * in[1] + in[0] * in[2]);
- out[3] = 2 * (in[1] * in[2] + in[0] * in[3]);
- out[4] = in[2] * in[2]
- + 4 * in[1] * in[3]
- + 2 * in[0] * in[4];
- out[5] = 2 * (in[2] * in[3] + in[1] * in[4] + in[0] * in[5]);
- out[6] = 2 * (in[3] * in[3] + in[2] * in[4] + in[0] * in[6] + 2 * in[1] * in[5]);
- out[7] = 2 * (in[3] * in[4] + in[2] * in[5] + in[1] * in[6] + in[0] * in[7]);
- out[8] = in[4] * in[4]
- + 2 * (in[2] * in[6] + in[0] * in[8] + 2 * (in[1] * in[7] + in[3] * in[5]));
- out[9] = 2 * (in[4] * in[5] + in[3] * in[6] + in[2] * in[7] + in[1] * in[8] + in[0] * in[9]);
- out[10] = 2 * (in[5] * in[5]
- + in[4] * in[6]
- + in[2] * in[8]
- + 2 * (in[3] * in[7] + in[1] * in[9]));
- out[11] = 2 * (in[5] * in[6] + in[4] * in[7] + in[3] * in[8] + in[2] * in[9]);
- out[12] = in[6] * in[6]
- + 2 * (in[4] * in[8] + 2 * (in[5] * in[7] + in[3] * in[9]));
- out[13] = 2 * (in[6] * in[7] + in[5] * in[8] + in[4] * in[9]);
- out[14] = 2 * (in[7] * in[7] + in[6] * in[8] + 2 * in[5] * in[9]);
- out[15] = 2 * (in[7] * in[8] + in[6] * in[9]);
- out[16] = in[8] * in[8] + 4 * in[7] * in[9];
- out[17] = 2 * in[8] * in[9];
- out[18] = 2 * in[9] * in[9];
- }
-
- /**
- * Returns in^2.
- *
- * On entry: The |in| argument is in reduced coefficients form and |in[i]| < 2^27.
- *
- * On exit: The |output| argument is in reduced coefficients form (indeed, one need only provide
- * storage for 10 limbs) and |out[i]| < 2^26.
- */
- static void square(long[] output, long[] in) {
- long[] t = new long[19];
- squareInner(t, in);
- // |t[i]| < 14*2^54 because the largest product of two limbs will be < 2^(27+27) and SquareInner
- // adds together, at most, 14 of those products.
- reduce(t, output);
- }
-
- /**
- * Takes a little-endian, 32-byte number and expands it into mixed radix form.
- */
- static long[] expand(byte[] input) {
- long[] output = new long[LIMB_CNT];
- for (int i = 0; i < LIMB_CNT; i++) {
- output[i] = ((((long) (input[EXPAND_START[i]] & 0xff))
- | ((long) (input[EXPAND_START[i] + 1] & 0xff)) << 8
- | ((long) (input[EXPAND_START[i] + 2] & 0xff)) << 16
- | ((long) (input[EXPAND_START[i] + 3] & 0xff)) << 24) >> EXPAND_SHIFT[i]) & MASK[i & 1];
- }
- return output;
- }
-
- /**
- * Takes a fully reduced mixed radix form number and contract it into a little-endian, 32-byte
- * array.
- *
- * On entry: |input_limbs[i]| < 2^26
- */
- @SuppressWarnings("NarrowingCompoundAssignment")
- static byte[] contract(long[] inputLimbs) {
- long[] input = Arrays.copyOf(inputLimbs, LIMB_CNT);
- for (int j = 0; j < 2; j++) {
- for (int i = 0; i < 9; i++) {
- // This calculation is a time-invariant way to make input[i] non-negative by borrowing
- // from the next-larger limb.
- int carry = -(int) ((input[i] & (input[i] >> 31)) >> SHIFT[i & 1]);
- input[i] = input[i] + (carry << SHIFT[i & 1]);
- input[i + 1] -= carry;
- }
-
- // There's no greater limb for input[9] to borrow from, but we can multiply by 19 and borrow
- // from input[0], which is valid mod 2^255-19.
- {
- int carry = -(int) ((input[9] & (input[9] >> 31)) >> 25);
- input[9] += (carry << 25);
- input[0] -= (carry * 19);
- }
-
- // After the first iteration, input[1..9] are non-negative and fit within 25 or 26 bits,
- // depending on position. However, input[0] may be negative.
- }
-
- // The first borrow-propagation pass above ended with every limb except (possibly) input[0]
- // non-negative.
- //
- // If input[0] was negative after the first pass, then it was because of a carry from input[9].
- // On entry, input[9] < 2^26 so the carry was, at most, one, since (2**26-1) >> 25 = 1. Thus
- // input[0] >= -19.
- //
- // In the second pass, each limb is decreased by at most one. Thus the second borrow-propagation
- // pass could only have wrapped around to decrease input[0] again if the first pass left
- // input[0] negative *and* input[1] through input[9] were all zero. In that case, input[1] is
- // now 2^25 - 1, and this last borrow-propagation step will leave input[1] non-negative.
- {
- int carry = -(int) ((input[0] & (input[0] >> 31)) >> 26);
- input[0] += (carry << 26);
- input[1] -= carry;
- }
-
- // All input[i] are now non-negative. However, there might be values between 2^25 and 2^26 in a
- // limb which is, nominally, 25 bits wide.
- for (int j = 0; j < 2; j++) {
- for (int i = 0; i < 9; i++) {
- int carry = (int) (input[i] >> SHIFT[i & 1]);
- input[i] &= MASK[i & 1];
- input[i + 1] += carry;
- }
- }
-
- {
- int carry = (int) (input[9] >> 25);
- input[9] &= 0x1ffffff;
- input[0] += 19 * carry;
- }
-
- // If the first carry-chain pass, just above, ended up with a carry from input[9], and that
- // caused input[0] to be out-of-bounds, then input[0] was < 2^26 + 2*19, because the carry was,
- // at most, two.
- //
- // If the second pass carried from input[9] again then input[0] is < 2*19 and the input[9] ->
- // input[0] carry didn't push input[0] out of bounds.
-
- // It still remains the case that input might be between 2^255-19 and 2^255. In this case,
- // input[1..9] must take their maximum value and input[0] must be >= (2^255-19) & 0x3ffffff,
- // which is 0x3ffffed.
- int mask = gte((int) input[0], 0x3ffffed);
- for (int i = 1; i < LIMB_CNT; i++) {
- mask &= eq((int) input[i], MASK[i & 1]);
- }
-
- // mask is either 0xffffffff (if input >= 2^255-19) and zero otherwise. Thus this conditionally
- // subtracts 2^255-19.
- input[0] -= mask & 0x3ffffed;
- input[1] -= mask & 0x1ffffff;
- for (int i = 2; i < LIMB_CNT; i += 2) {
- input[i] -= mask & 0x3ffffff;
- input[i + 1] -= mask & 0x1ffffff;
- }
-
- for (int i = 0; i < LIMB_CNT; i++) {
- input[i] <<= EXPAND_SHIFT[i];
- }
- byte[] output = new byte[FIELD_LEN];
- for (int i = 0; i < LIMB_CNT; i++) {
- output[EXPAND_START[i]] |= input[i] & 0xff;
- output[EXPAND_START[i] + 1] |= (input[i] >> 8) & 0xff;
- output[EXPAND_START[i] + 2] |= (input[i] >> 16) & 0xff;
- output[EXPAND_START[i] + 3] |= (input[i] >> 24) & 0xff;
- }
- return output;
- }
-
- /**
- * Computes inverse of z = z(2^255 - 21)
- *
- * Shamelessly copied from agl's code which was shamelessly copied from djb's code. Only the
- * comment format and the variable namings are different from those.
- */
- static void inverse(long[] out, long[] z) {
- long[] z2 = new long[Field25519.LIMB_CNT];
- long[] z9 = new long[Field25519.LIMB_CNT];
- long[] z11 = new long[Field25519.LIMB_CNT];
- long[] z2To5Minus1 = new long[Field25519.LIMB_CNT];
- long[] z2To10Minus1 = new long[Field25519.LIMB_CNT];
- long[] z2To20Minus1 = new long[Field25519.LIMB_CNT];
- long[] z2To50Minus1 = new long[Field25519.LIMB_CNT];
- long[] z2To100Minus1 = new long[Field25519.LIMB_CNT];
- long[] t0 = new long[Field25519.LIMB_CNT];
- long[] t1 = new long[Field25519.LIMB_CNT];
-
- square(z2, z); // 2
- square(t1, z2); // 4
- square(t0, t1); // 8
- mult(z9, t0, z); // 9
- mult(z11, z9, z2); // 11
- square(t0, z11); // 22
- mult(z2To5Minus1, t0, z9); // 2^5 - 2^0 = 31
-
- square(t0, z2To5Minus1); // 2^6 - 2^1
- square(t1, t0); // 2^7 - 2^2
- square(t0, t1); // 2^8 - 2^3
- square(t1, t0); // 2^9 - 2^4
- square(t0, t1); // 2^10 - 2^5
- mult(z2To10Minus1, t0, z2To5Minus1); // 2^10 - 2^0
-
- square(t0, z2To10Minus1); // 2^11 - 2^1
- square(t1, t0); // 2^12 - 2^2
- for (int i = 2; i < 10; i += 2) { // 2^20 - 2^10
- square(t0, t1);
- square(t1, t0);
- }
- mult(z2To20Minus1, t1, z2To10Minus1); // 2^20 - 2^0
-
- square(t0, z2To20Minus1); // 2^21 - 2^1
- square(t1, t0); // 2^22 - 2^2
- for (int i = 2; i < 20; i += 2) { // 2^40 - 2^20
- square(t0, t1);
- square(t1, t0);
- }
- mult(t0, t1, z2To20Minus1); // 2^40 - 2^0
-
- square(t1, t0); // 2^41 - 2^1
- square(t0, t1); // 2^42 - 2^2
- for (int i = 2; i < 10; i += 2) { // 2^50 - 2^10
- square(t1, t0);
- square(t0, t1);
- }
- mult(z2To50Minus1, t0, z2To10Minus1); // 2^50 - 2^0
-
- square(t0, z2To50Minus1); // 2^51 - 2^1
- square(t1, t0); // 2^52 - 2^2
- for (int i = 2; i < 50; i += 2) { // 2^100 - 2^50
- square(t0, t1);
- square(t1, t0);
- }
- mult(z2To100Minus1, t1, z2To50Minus1); // 2^100 - 2^0
-
- square(t1, z2To100Minus1); // 2^101 - 2^1
- square(t0, t1); // 2^102 - 2^2
- for (int i = 2; i < 100; i += 2) { // 2^200 - 2^100
- square(t1, t0);
- square(t0, t1);
- }
- mult(t1, t0, z2To100Minus1); // 2^200 - 2^0
-
- square(t0, t1); // 2^201 - 2^1
- square(t1, t0); // 2^202 - 2^2
- for (int i = 2; i < 50; i += 2) { // 2^250 - 2^50
- square(t0, t1);
- square(t1, t0);
- }
- mult(t0, t1, z2To50Minus1); // 2^250 - 2^0
-
- square(t1, t0); // 2^251 - 2^1
- square(t0, t1); // 2^252 - 2^2
- square(t1, t0); // 2^253 - 2^3
- square(t0, t1); // 2^254 - 2^4
- square(t1, t0); // 2^255 - 2^5
- mult(out, t1, z11); // 2^255 - 21
- }
-
-
- /**
- * Returns 0xffffffff iff a == b and zero otherwise.
- */
- private static int eq(int a, int b) {
- a = ~(a ^ b);
- a &= a << 16;
- a &= a << 8;
- a &= a << 4;
- a &= a << 2;
- a &= a << 1;
- return a >> 31;
- }
-
- /**
- * returns 0xffffffff if a >= b and zero otherwise, where a and b are both non-negative.
- */
- private static int gte(int a, int b) {
- a -= b;
- // a >= 0 iff a >= b.
- return ~(a >> 31);
- }
- }
-
- // (x = 0, y = 1) point
- private static final CachedXYT CACHED_NEUTRAL = new CachedXYT(
- new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
- private static final PartialXYZT NEUTRAL = new PartialXYZT(
- new XYZ(new long[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0},
- new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}),
- new long[]{1, 0, 0, 0, 0, 0, 0, 0, 0, 0});
-
- /**
- * Projective point representation (X:Y:Z) satisfying x = X/Z, y = Y/Z
- *
- * Note that this is referred as ge_p2 in ref10 impl.
- * Also note that x = X, y = Y and z = Z below following Java coding style.
- *
- * See
- * Koyama K., Tsuruoka Y. (1993) Speeding up Elliptic Cryptosystems by Using a Signed Binary
- * Window Method.
- *
- * https://hyperelliptic.org/EFD/g1p/auto-twisted-projective.html
- */
- private static class XYZ {
-
- final long[] x;
- final long[] y;
- final long[] z;
-
- XYZ() {
- this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);
- }
-
- XYZ(long[] x, long[] y, long[] z) {
- this.x = x;
- this.y = y;
- this.z = z;
- }
-
- XYZ(XYZ xyz) {
- x = Arrays.copyOf(xyz.x, Field25519.LIMB_CNT);
- y = Arrays.copyOf(xyz.y, Field25519.LIMB_CNT);
- z = Arrays.copyOf(xyz.z, Field25519.LIMB_CNT);
- }
-
- XYZ(PartialXYZT partialXYZT) {
- this();
- fromPartialXYZT(this, partialXYZT);
- }
-
- /**
- * ge_p1p1_to_p2.c
- */
- static XYZ fromPartialXYZT(XYZ out, PartialXYZT in) {
- Field25519.mult(out.x, in.xyz.x, in.t);
- Field25519.mult(out.y, in.xyz.y, in.xyz.z);
- Field25519.mult(out.z, in.xyz.z, in.t);
- return out;
- }
-
- /**
- * Encodes this point to bytes.
- */
- byte[] toBytes() {
- long[] recip = new long[Field25519.LIMB_CNT];
- long[] x = new long[Field25519.LIMB_CNT];
- long[] y = new long[Field25519.LIMB_CNT];
- Field25519.inverse(recip, z);
- Field25519.mult(x, this.x, recip);
- Field25519.mult(y, this.y, recip);
- byte[] s = Field25519.contract(y);
- s[31] = (byte) (s[31] ^ (getLsb(x) << 7));
- return s;
- }
-
-
- /**
- * Best effort fix-timing array comparison.
- *
- * @return true if two arrays are equal.
- */
- private static boolean bytesEqual(final byte[] x, final byte[] y) {
- if (x == null || y == null) {
- return false;
- }
- if (x.length != y.length) {
- return false;
- }
- int res = 0;
- for (int i = 0; i < x.length; i++) {
- res |= x[i] ^ y[i];
- }
- return res == 0;
- }
-
- /**
- * Checks that the point is on curve
- */
- boolean isOnCurve() {
- long[] x2 = new long[Field25519.LIMB_CNT];
- Field25519.square(x2, x);
- long[] y2 = new long[Field25519.LIMB_CNT];
- Field25519.square(y2, y);
- long[] z2 = new long[Field25519.LIMB_CNT];
- Field25519.square(z2, z);
- long[] z4 = new long[Field25519.LIMB_CNT];
- Field25519.square(z4, z2);
- long[] lhs = new long[Field25519.LIMB_CNT];
- // lhs = y^2 - x^2
- Field25519.sub(lhs, y2, x2);
- // lhs = z^2 * (y2 - x2)
- Field25519.mult(lhs, lhs, z2);
- long[] rhs = new long[Field25519.LIMB_CNT];
- // rhs = x^2 * y^2
- Field25519.mult(rhs, x2, y2);
- // rhs = D * x^2 * y^2
- Field25519.mult(rhs, rhs, D);
- // rhs = z^4 + D * x^2 * y^2
- Field25519.sum(rhs, z4);
- // Field25519.mult reduces its output, but Field25519.sum does not, so we have to manually
- // reduce it here.
- Field25519.reduce(rhs, rhs);
- // z^2 (y^2 - x^2) == z^4 + D * x^2 * y^2
- return bytesEqual(Field25519.contract(lhs), Field25519.contract(rhs));
- }
- }
-
- /**
- * Represents extended projective point representation (X:Y:Z:T) satisfying x = X/Z, y = Y/Z,
- * XY = ZT
- *
- * Note that this is referred as ge_p3 in ref10 impl.
- * Also note that t = T below following Java coding style.
- *
- * See
- * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
- *
- * https://hyperelliptic.org/EFD/g1p/auto-twisted-extended.html
- */
- private static class XYZT {
-
- final XYZ xyz;
- final long[] t;
-
- XYZT() {
- this(new XYZ(), new long[Field25519.LIMB_CNT]);
- }
-
- XYZT(XYZ xyz, long[] t) {
- this.xyz = xyz;
- this.t = t;
- }
-
- XYZT(PartialXYZT partialXYZT) {
- this();
- fromPartialXYZT(this, partialXYZT);
- }
-
- /**
- * ge_p1p1_to_p2.c
- */
- private static XYZT fromPartialXYZT(XYZT out, PartialXYZT in) {
- Field25519.mult(out.xyz.x, in.xyz.x, in.t);
- Field25519.mult(out.xyz.y, in.xyz.y, in.xyz.z);
- Field25519.mult(out.xyz.z, in.xyz.z, in.t);
- Field25519.mult(out.t, in.xyz.x, in.xyz.y);
- return out;
- }
-
- /**
- * Decodes {@code s} into an extented projective point.
- * See Section 5.1.3 Decoding in https://tools.ietf.org/html/rfc8032#section-5.1.3
- */
- private static XYZT fromBytesNegateVarTime(byte[] s) throws GeneralSecurityException {
- long[] x = new long[Field25519.LIMB_CNT];
- long[] y = Field25519.expand(s);
- long[] z = new long[Field25519.LIMB_CNT];
- z[0] = 1;
- long[] t = new long[Field25519.LIMB_CNT];
- long[] u = new long[Field25519.LIMB_CNT];
- long[] v = new long[Field25519.LIMB_CNT];
- long[] vxx = new long[Field25519.LIMB_CNT];
- long[] check = new long[Field25519.LIMB_CNT];
- Field25519.square(u, y);
- Field25519.mult(v, u, D);
- Field25519.sub(u, u, z); // u = y^2 - 1
- Field25519.sum(v, v, z); // v = dy^2 + 1
-
- long[] v3 = new long[Field25519.LIMB_CNT];
- Field25519.square(v3, v);
- Field25519.mult(v3, v3, v); // v3 = v^3
- Field25519.square(x, v3);
- Field25519.mult(x, x, v);
- Field25519.mult(x, x, u); // x = uv^7
-
- pow2252m3(x, x); // x = (uv^7)^((q-5)/8)
- Field25519.mult(x, x, v3);
- Field25519.mult(x, x, u); // x = uv^3(uv^7)^((q-5)/8)
-
- Field25519.square(vxx, x);
- Field25519.mult(vxx, vxx, v);
- Field25519.sub(check, vxx, u); // vx^2-u
- if (isNonZeroVarTime(check)) {
- Field25519.sum(check, vxx, u); // vx^2+u
- if (isNonZeroVarTime(check)) {
- throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
- + "coordinates. No square root exists for modulo 2^255-19");
- }
- Field25519.mult(x, x, SQRTM1);
- }
-
- if (!isNonZeroVarTime(x) && (s[31] & 0xff) >> 7 != 0) {
- throw new GeneralSecurityException("Cannot convert given bytes to extended projective "
- + "coordinates. Computed x is zero and encoded x's least significant bit is not zero");
- }
- if (getLsb(x) == ((s[31] & 0xff) >> 7)) {
- neg(x, x);
- }
-
- Field25519.mult(t, x, y);
- return new XYZT(new XYZ(x, y, z), t);
- }
- }
-
- /**
- * Partial projective point representation ((X:Z),(Y:T)) satisfying x=X/Z, y=Y/T
- *
- * Note that this is referred as complete form in the original ref10 impl (ge_p1p1).
- * Also note that t = T below following Java coding style.
- *
- * Although this has the same types as XYZT, it is redefined to have its own type so that it is
- * readable and 1:1 corresponds to ref10 impl.
- *
- * Can be converted to XYZT as follows:
- * X1 = X * T = x * Z * T = x * Z1
- * Y1 = Y * Z = y * T * Z = y * Z1
- * Z1 = Z * T = Z * T
- * T1 = X * Y = x * Z * y * T = x * y * Z1 = X1Y1 / Z1
- */
- private static class PartialXYZT {
-
- final XYZ xyz;
- final long[] t;
-
- PartialXYZT() {
- this(new XYZ(), new long[Field25519.LIMB_CNT]);
- }
-
- PartialXYZT(XYZ xyz, long[] t) {
- this.xyz = xyz;
- this.t = t;
- }
-
- PartialXYZT(PartialXYZT other) {
- xyz = new XYZ(other.xyz);
- t = Arrays.copyOf(other.t, Field25519.LIMB_CNT);
- }
- }
-
- /**
- * Corresponds to the caching mentioned in the last paragraph of Section 3.1 of
- * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
- * with Z = 1.
- */
- private static class CachedXYT {
-
- final long[] yPlusX;
- final long[] yMinusX;
- final long[] t2d;
-
- /**
- * Creates a cached XYZT with Z = 1
- *
- * @param yPlusX y + x
- * @param yMinusX y - x
- * @param t2d 2d * xy
- */
- CachedXYT(long[] yPlusX, long[] yMinusX, long[] t2d) {
- this.yPlusX = yPlusX;
- this.yMinusX = yMinusX;
- this.t2d = t2d;
- }
-
- CachedXYT(CachedXYT other) {
- yPlusX = Arrays.copyOf(other.yPlusX, Field25519.LIMB_CNT);
- yMinusX = Arrays.copyOf(other.yMinusX, Field25519.LIMB_CNT);
- t2d = Arrays.copyOf(other.t2d, Field25519.LIMB_CNT);
- }
-
- // z is one implicitly, so this just copies {@code in} to {@code output}.
- void multByZ(long[] output, long[] in) {
- System.arraycopy(in, 0, output, 0, Field25519.LIMB_CNT);
- }
-
- /**
- * If icopy is 1, copies {@code other} into this point. Time invariant wrt to icopy value.
- */
- void copyConditional(CachedXYT other, int icopy) {
- copyConditional(yPlusX, other.yPlusX, icopy);
- copyConditional(yMinusX, other.yMinusX, icopy);
- copyConditional(t2d, other.t2d, icopy);
- }
-
- /**
- * Conditionally copies a reduced-form limb arrays {@code b} into {@code a} if {@code icopy} is 1,
- * but leave {@code a} unchanged if 'iswap' is 0. Runs in data-invariant time to avoid
- * side-channel attacks.
- *
- *
NOTE that this function requires that {@code icopy} be 1 or 0; other values give wrong
- * results. Also, the two limb arrays must be in reduced-coefficient, reduced-degree form: the
- * values in a[10..19] or b[10..19] aren't swapped, and all all values in a[0..9],b[0..9] must
- * have magnitude less than Integer.MAX_VALUE.
- */
- static void copyConditional(long[] a, long[] b, int icopy) {
- int copy = -icopy;
- for (int i = 0; i < Field25519.LIMB_CNT; i++) {
- int x = copy & (((int) a[i]) ^ ((int) b[i]));
- a[i] = ((int) a[i]) ^ x;
- }
- }
- }
-
- private static class CachedXYZT extends CachedXYT {
-
- private final long[] z;
-
- CachedXYZT() {
- this(new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT], new long[Field25519.LIMB_CNT]);
- }
-
- /**
- * ge_p3_to_cached.c
- */
- CachedXYZT(XYZT xyzt) {
- this();
- Field25519.sum(yPlusX, xyzt.xyz.y, xyzt.xyz.x);
- Field25519.sub(yMinusX, xyzt.xyz.y, xyzt.xyz.x);
- System.arraycopy(xyzt.xyz.z, 0, z, 0, Field25519.LIMB_CNT);
- Field25519.mult(t2d, xyzt.t, D2);
- }
-
- /**
- * Creates a cached XYZT
- *
- * @param yPlusX Y + X
- * @param yMinusX Y - X
- * @param z Z
- * @param t2d 2d * (XY/Z)
- */
- CachedXYZT(long[] yPlusX, long[] yMinusX, long[] z, long[] t2d) {
- super(yPlusX, yMinusX, t2d);
- this.z = z;
- }
-
- @Override
- public void multByZ(long[] output, long[] in) {
- Field25519.mult(output, in, z);
- }
- }
-
- /**
- * Addition defined in Section 3.1 of
- * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
- *
- * Please note that this is a partial of the operation listed there leaving out the final
- * conversion from PartialXYZT to XYZT.
- *
- * @param extended extended projective point input
- * @param cached cached projective point input
- */
- private static void add(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
- long[] t = new long[Field25519.LIMB_CNT];
-
- // Y1 + X1
- Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
-
- // Y1 - X1
- Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
-
- // A = (Y1 - X1) * (Y2 - X2)
- Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yMinusX);
-
- // B = (Y1 + X1) * (Y2 + X2)
- Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yPlusX);
-
- // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
- Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
-
- // Z1 * Z2
- cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
-
- // D = 2 * Z1 * Z2
- Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
-
- // X3 = B - A
- Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
- // Y3 = B + A
- Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
- // Z3 = D + C
- Field25519.sum(partialXYZT.xyz.z, t, partialXYZT.t);
-
- // T3 = D - C
- Field25519.sub(partialXYZT.t, t, partialXYZT.t);
- }
-
- /**
- * Based on the addition defined in Section 3.1 of
- * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
- *
- * Please note that this is a partial of the operation listed there leaving out the final
- * conversion from PartialXYZT to XYZT.
- *
- * @param extended extended projective point input
- * @param cached cached projective point input
- */
- private static void sub(PartialXYZT partialXYZT, XYZT extended, CachedXYT cached) {
- long[] t = new long[Field25519.LIMB_CNT];
-
- // Y1 + X1
- Field25519.sum(partialXYZT.xyz.x, extended.xyz.y, extended.xyz.x);
-
- // Y1 - X1
- Field25519.sub(partialXYZT.xyz.y, extended.xyz.y, extended.xyz.x);
-
- // A = (Y1 - X1) * (Y2 + X2)
- Field25519.mult(partialXYZT.xyz.y, partialXYZT.xyz.y, cached.yPlusX);
-
- // B = (Y1 + X1) * (Y2 - X2)
- Field25519.mult(partialXYZT.xyz.z, partialXYZT.xyz.x, cached.yMinusX);
-
- // C = T1 * 2d * T2 = 2d * T1 * T2 (2d is written as k in the paper)
- Field25519.mult(partialXYZT.t, extended.t, cached.t2d);
-
- // Z1 * Z2
- cached.multByZ(partialXYZT.xyz.x, extended.xyz.z);
-
- // D = 2 * Z1 * Z2
- Field25519.sum(t, partialXYZT.xyz.x, partialXYZT.xyz.x);
-
- // X3 = B - A
- Field25519.sub(partialXYZT.xyz.x, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
- // Y3 = B + A
- Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.y);
-
- // Z3 = D - C
- Field25519.sub(partialXYZT.xyz.z, t, partialXYZT.t);
-
- // T3 = D + C
- Field25519.sum(partialXYZT.t, t, partialXYZT.t);
- }
-
- /**
- * Doubles {@code p} and puts the result into this PartialXYZT.
- *
- * This is based on the addition defined in formula 7 in Section 3.3 of
- * Hisil H., Wong K.KH., Carter G., Dawson E. (2008) Twisted Edwards Curves Revisited.
- *
- * Please note that this is a partial of the operation listed there leaving out the final
- * conversion from PartialXYZT to XYZT and also this fixes a typo in calculation of Y3 and T3 in
- * the paper, H should be replaced with A+B.
- */
- private static void doubleXYZ(PartialXYZT partialXYZT, XYZ p) {
- long[] t0 = new long[Field25519.LIMB_CNT];
-
- // XX = X1^2
- Field25519.square(partialXYZT.xyz.x, p.x);
-
- // YY = Y1^2
- Field25519.square(partialXYZT.xyz.z, p.y);
-
- // B' = Z1^2
- Field25519.square(partialXYZT.t, p.z);
-
- // B = 2 * B'
- Field25519.sum(partialXYZT.t, partialXYZT.t, partialXYZT.t);
-
- // A = X1 + Y1
- Field25519.sum(partialXYZT.xyz.y, p.x, p.y);
-
- // AA = A^2
- Field25519.square(t0, partialXYZT.xyz.y);
-
- // Y3 = YY + XX
- Field25519.sum(partialXYZT.xyz.y, partialXYZT.xyz.z, partialXYZT.xyz.x);
-
- // Z3 = YY - XX
- Field25519.sub(partialXYZT.xyz.z, partialXYZT.xyz.z, partialXYZT.xyz.x);
-
- // X3 = AA - Y3
- Field25519.sub(partialXYZT.xyz.x, t0, partialXYZT.xyz.y);
-
- // T3 = B - Z3
- Field25519.sub(partialXYZT.t, partialXYZT.t, partialXYZT.xyz.z);
- }
-
- /**
- * Doubles {@code p} and puts the result into this PartialXYZT.
- */
- private static void doubleXYZT(PartialXYZT partialXYZT, XYZT p) {
- doubleXYZ(partialXYZT, p.xyz);
- }
-
- /**
- * Compares two byte values in constant time.
- */
- private static int eq(int a, int b) {
- int r = ~(a ^ b) & 0xff;
- r &= r << 4;
- r &= r << 2;
- r &= r << 1;
- return (r >> 7) & 1;
- }
-
- /**
- * This is a constant time operation where point b*B*256^pos is stored in {@code t}.
- * When b is 0, t remains the same (i.e., neutral point).
- *
- * Although B_TABLE[32][8] (B_TABLE[i][j] = (j+1)*B*256^i) has j values in [0, 7], the select
- * method negates the corresponding point if b is negative (which is straight forward in elliptic
- * curves by just negating y coordinate). Therefore we can get multiples of B with the half of
- * memory requirements.
- *
- * @param t neutral element (i.e., point 0), also serves as output.
- * @param pos in B[pos][j] = (j+1)*B*256^pos
- * @param b value in [-8, 8] range.
- */
- private static void select(CachedXYT t, int pos, byte b) {
- int bnegative = (b & 0xff) >> 7;
- int babs = b - (((-bnegative) & b) << 1);
-
- t.copyConditional(B_TABLE[pos][0], eq(babs, 1));
- t.copyConditional(B_TABLE[pos][1], eq(babs, 2));
- t.copyConditional(B_TABLE[pos][2], eq(babs, 3));
- t.copyConditional(B_TABLE[pos][3], eq(babs, 4));
- t.copyConditional(B_TABLE[pos][4], eq(babs, 5));
- t.copyConditional(B_TABLE[pos][5], eq(babs, 6));
- t.copyConditional(B_TABLE[pos][6], eq(babs, 7));
- t.copyConditional(B_TABLE[pos][7], eq(babs, 8));
-
- long[] yPlusX = Arrays.copyOf(t.yMinusX, Field25519.LIMB_CNT);
- long[] yMinusX = Arrays.copyOf(t.yPlusX, Field25519.LIMB_CNT);
- long[] t2d = Arrays.copyOf(t.t2d, Field25519.LIMB_CNT);
- neg(t2d, t2d);
- CachedXYT minust = new CachedXYT(yPlusX, yMinusX, t2d);
- t.copyConditional(minust, bnegative);
- }
-
- /**
- * Computes {@code a}*B
- * where a = a[0]+256*a[1]+...+256^31 a[31] and
- * B is the Ed25519 base point (x,4/5) with x positive.
- *
- * Preconditions:
- * a[31] <= 127
- *
- * @throws IllegalStateException iff there is arithmetic error.
- */
- @SuppressWarnings("NarrowingCompoundAssignment")
- private static XYZ scalarMultWithBase(byte[] a) {
- byte[] e = new byte[2 * Field25519.FIELD_LEN];
- for (int i = 0; i < Field25519.FIELD_LEN; i++) {
- e[2 * i + 0] = (byte) (((a[i] & 0xff) >> 0) & 0xf);
- e[2 * i + 1] = (byte) (((a[i] & 0xff) >> 4) & 0xf);
- }
- // each e[i] is between 0 and 15
- // e[63] is between 0 and 7
-
- // Rewrite e in a way that each e[i] is in [-8, 8].
- // This can be done since a[63] is in [0, 7], the carry-over onto the most significant byte
- // a[63] can be at most 1.
- int carry = 0;
- for (int i = 0; i < e.length - 1; i++) {
- e[i] += carry;
- carry = e[i] + 8;
- carry >>= 4;
- e[i] -= carry << 4;
- }
- e[e.length - 1] += carry;
-
- PartialXYZT ret = new PartialXYZT(NEUTRAL);
- XYZT xyzt = new XYZT();
- // Although B_TABLE's i can be at most 31 (stores only 32 4bit multiples of B) and we have 64
- // 4bit values in e array, the below for loop adds cached values by iterating e by two in odd
- // indices. After the result, we can double the result point 4 times to shift the multiplication
- // scalar by 4 bits.
- for (int i = 1; i < e.length; i += 2) {
- CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
- select(t, i / 2, e[i]);
- add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
- }
-
- // Doubles the result 4 times to shift the multiplication scalar 4 bits to get the actual result
- // for the odd indices in e.
- XYZ xyz = new XYZ();
- doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
- doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
- doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
- doubleXYZ(ret, XYZ.fromPartialXYZT(xyz, ret));
-
- // Add multiples of B for even indices of e.
- for (int i = 0; i < e.length; i += 2) {
- CachedXYT t = new CachedXYT(CACHED_NEUTRAL);
- select(t, i / 2, e[i]);
- add(ret, XYZT.fromPartialXYZT(xyzt, ret), t);
- }
-
- // This check is to protect against flaws, i.e. if there is a computation error through a
- // faulty CPU or if the implementation contains a bug.
- XYZ result = new XYZ(ret);
- if (!result.isOnCurve()) {
- throw new IllegalStateException("arithmetic error in scalar multiplication");
- }
- return result;
- }
-
- @SuppressWarnings("NarrowingCompoundAssignment")
- private static byte[] slide(byte[] a) {
- byte[] r = new byte[256];
- // Writes each bit in a[0..31] into r[0..255]:
- // a = a[0]+256*a[1]+...+256^31*a[31] is equal to
- // r = r[0]+2*r[1]+...+2^255*r[255]
- for (int i = 0; i < 256; i++) {
- r[i] = (byte) (1 & ((a[i >> 3] & 0xff) >> (i & 7)));
- }
-
- // Transforms r[i] as odd values in [-15, 15]
- for (int i = 0; i < 256; i++) {
- if (r[i] != 0) {
- for (int b = 1; b <= 6 && i + b < 256; b++) {
- if (r[i + b] != 0) {
- if (r[i] + (r[i + b] << b) <= 15) {
- r[i] += r[i + b] << b;
- r[i + b] = 0;
- } else if (r[i] - (r[i + b] << b) >= -15) {
- r[i] -= r[i + b] << b;
- for (int k = i + b; k < 256; k++) {
- if (r[k] == 0) {
- r[k] = 1;
- break;
- }
- r[k] = 0;
- }
- } else {
- break;
- }
- }
- }
- }
- }
- return r;
- }
-
- /**
- * Computes {@code a}*{@code pointA}+{@code b}*B
- * where a = a[0]+256*a[1]+...+256^31*a[31].
- * and b = b[0]+256*b[1]+...+256^31*b[31].
- * B is the Ed25519 base point (x,4/5) with x positive.
- *
- * Note that execution time varies based on the input since this will only be used in verification
- * of signatures.
- */
- private static XYZ doubleScalarMultVarTime(byte[] a, XYZT pointA, byte[] b) {
- // pointA, 3*pointA, 5*pointA, 7*pointA, 9*pointA, 11*pointA, 13*pointA, 15*pointA
- CachedXYZT[] pointAArray = new CachedXYZT[8];
- pointAArray[0] = new CachedXYZT(pointA);
- PartialXYZT t = new PartialXYZT();
- doubleXYZT(t, pointA);
- XYZT doubleA = new XYZT(t);
- for (int i = 1; i < pointAArray.length; i++) {
- add(t, doubleA, pointAArray[i - 1]);
- pointAArray[i] = new CachedXYZT(new XYZT(t));
- }
-
- byte[] aSlide = slide(a);
- byte[] bSlide = slide(b);
- t = new PartialXYZT(NEUTRAL);
- XYZT u = new XYZT();
- int i = 255;
- for (; i >= 0; i--) {
- if (aSlide[i] != 0 || bSlide[i] != 0) {
- break;
- }
- }
- for (; i >= 0; i--) {
- doubleXYZ(t, new XYZ(t));
- if (aSlide[i] > 0) {
- add(t, XYZT.fromPartialXYZT(u, t), pointAArray[aSlide[i] / 2]);
- } else if (aSlide[i] < 0) {
- sub(t, XYZT.fromPartialXYZT(u, t), pointAArray[-aSlide[i] / 2]);
- }
- if (bSlide[i] > 0) {
- add(t, XYZT.fromPartialXYZT(u, t), B2[bSlide[i] / 2]);
- } else if (bSlide[i] < 0) {
- sub(t, XYZT.fromPartialXYZT(u, t), B2[-bSlide[i] / 2]);
- }
- }
-
- return new XYZ(t);
- }
-
- /**
- * Returns true if {@code in} is nonzero.
- *
- * Note that execution time might depend on the input {@code in}.
- */
- private static boolean isNonZeroVarTime(long[] in) {
- long[] inCopy = new long[in.length + 1];
- System.arraycopy(in, 0, inCopy, 0, in.length);
- Field25519.reduceCoefficients(inCopy);
- byte[] bytes = Field25519.contract(inCopy);
- for (byte b : bytes) {
- if (b != 0) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Returns the least significant bit of {@code in}.
- */
- private static int getLsb(long[] in) {
- return Field25519.contract(in)[0] & 1;
- }
-
- /**
- * Negates all values in {@code in} and store it in {@code out}.
- */
- private static void neg(long[] out, long[] in) {
- for (int i = 0; i < in.length; i++) {
- out[i] = -in[i];
- }
- }
-
- /**
- * Computes {@code in}^(2^252-3) mod 2^255-19 and puts the result in {@code out}.
- */
- private static void pow2252m3(long[] out, long[] in) {
- long[] t0 = new long[Field25519.LIMB_CNT];
- long[] t1 = new long[Field25519.LIMB_CNT];
- long[] t2 = new long[Field25519.LIMB_CNT];
-
- // z2 = z1^2^1
- Field25519.square(t0, in);
-
- // z8 = z2^2^2
- Field25519.square(t1, t0);
- for (int i = 1; i < 2; i++) {
- Field25519.square(t1, t1);
- }
-
- // z9 = z1*z8
- Field25519.mult(t1, in, t1);
-
- // z11 = z2*z9
- Field25519.mult(t0, t0, t1);
-
- // z22 = z11^2^1
- Field25519.square(t0, t0);
-
- // z_5_0 = z9*z22
- Field25519.mult(t0, t1, t0);
-
- // z_10_5 = z_5_0^2^5
- Field25519.square(t1, t0);
- for (int i = 1; i < 5; i++) {
- Field25519.square(t1, t1);
- }
-
- // z_10_0 = z_10_5*z_5_0
- Field25519.mult(t0, t1, t0);
-
- // z_20_10 = z_10_0^2^10
- Field25519.square(t1, t0);
- for (int i = 1; i < 10; i++) {
- Field25519.square(t1, t1);
- }
-
- // z_20_0 = z_20_10*z_10_0
- Field25519.mult(t1, t1, t0);
-
- // z_40_20 = z_20_0^2^20
- Field25519.square(t2, t1);
- for (int i = 1; i < 20; i++) {
- Field25519.square(t2, t2);
- }
-
- // z_40_0 = z_40_20*z_20_0
- Field25519.mult(t1, t2, t1);
-
- // z_50_10 = z_40_0^2^10
- Field25519.square(t1, t1);
- for (int i = 1; i < 10; i++) {
- Field25519.square(t1, t1);
- }
-
- // z_50_0 = z_50_10*z_10_0
- Field25519.mult(t0, t1, t0);
-
- // z_100_50 = z_50_0^2^50
- Field25519.square(t1, t0);
- for (int i = 1; i < 50; i++) {
- Field25519.square(t1, t1);
- }
-
- // z_100_0 = z_100_50*z_50_0
- Field25519.mult(t1, t1, t0);
-
- // z_200_100 = z_100_0^2^100
- Field25519.square(t2, t1);
- for (int i = 1; i < 100; i++) {
- Field25519.square(t2, t2);
- }
-
- // z_200_0 = z_200_100*z_100_0
- Field25519.mult(t1, t2, t1);
-
- // z_250_50 = z_200_0^2^50
- Field25519.square(t1, t1);
- for (int i = 1; i < 50; i++) {
- Field25519.square(t1, t1);
- }
-
- // z_250_0 = z_250_50*z_50_0
- Field25519.mult(t0, t1, t0);
-
- // z_252_2 = z_250_0^2^2
- Field25519.square(t0, t0);
- for (int i = 1; i < 2; i++) {
- Field25519.square(t0, t0);
- }
-
- // z_252_3 = z_252_2*z1
- Field25519.mult(out, t0, in);
- }
-
- /**
- * Returns 3 bytes of {@code in} starting from {@code idx} in Little-Endian format.
- */
- private static long load3(byte[] in, int idx) {
- long result;
- result = (long) in[idx] & 0xff;
- result |= (long) (in[idx + 1] & 0xff) << 8;
- result |= (long) (in[idx + 2] & 0xff) << 16;
- return result;
- }
-
- /**
- * Returns 4 bytes of {@code in} starting from {@code idx} in Little-Endian format.
- */
- private static long load4(byte[] in, int idx) {
- long result = load3(in, idx);
- result |= (long) (in[idx + 3] & 0xff) << 24;
- return result;
- }
-
- /**
- * Input:
- * s[0]+256*s[1]+...+256^63*s[63] = s
- *
- * Output:
- * s[0]+256*s[1]+...+256^31*s[31] = s mod l
- * where l = 2^252 + 27742317777372353535851937790883648493.
- * Overwrites s in place.
- */
- private static void reduce(byte[] s) {
- // Observation:
- // 2^252 mod l is equivalent to -27742317777372353535851937790883648493 mod l
- // Let m = -27742317777372353535851937790883648493
- // Thus a*2^252+b mod l is equivalent to a*m+b mod l
- //
- // First s is divided into chunks of 21 bits as follows:
- // s0+2^21*s1+2^42*s3+...+2^462*s23 = s[0]+256*s[1]+...+256^63*s[63]
- long s0 = 2097151 & load3(s, 0);
- long s1 = 2097151 & (load4(s, 2) >> 5);
- long s2 = 2097151 & (load3(s, 5) >> 2);
- long s3 = 2097151 & (load4(s, 7) >> 7);
- long s4 = 2097151 & (load4(s, 10) >> 4);
- long s5 = 2097151 & (load3(s, 13) >> 1);
- long s6 = 2097151 & (load4(s, 15) >> 6);
- long s7 = 2097151 & (load3(s, 18) >> 3);
- long s8 = 2097151 & load3(s, 21);
- long s9 = 2097151 & (load4(s, 23) >> 5);
- long s10 = 2097151 & (load3(s, 26) >> 2);
- long s11 = 2097151 & (load4(s, 28) >> 7);
- long s12 = 2097151 & (load4(s, 31) >> 4);
- long s13 = 2097151 & (load3(s, 34) >> 1);
- long s14 = 2097151 & (load4(s, 36) >> 6);
- long s15 = 2097151 & (load3(s, 39) >> 3);
- long s16 = 2097151 & load3(s, 42);
- long s17 = 2097151 & (load4(s, 44) >> 5);
- long s18 = 2097151 & (load3(s, 47) >> 2);
- long s19 = 2097151 & (load4(s, 49) >> 7);
- long s20 = 2097151 & (load4(s, 52) >> 4);
- long s21 = 2097151 & (load3(s, 55) >> 1);
- long s22 = 2097151 & (load4(s, 57) >> 6);
- long s23 = (load4(s, 60) >> 3);
- long carry0;
- long carry1;
- long carry2;
- long carry3;
- long carry4;
- long carry5;
- long carry6;
- long carry7;
- long carry8;
- long carry9;
- long carry10;
- long carry11;
- long carry12;
- long carry13;
- long carry14;
- long carry15;
- long carry16;
-
- // s23*2^462 = s23*2^210*2^252 is equivalent to s23*2^210*m in mod l
- // As m is a 125 bit number, the result needs to scattered to 6 limbs (125/21 ceil is 6)
- // starting from s11 (s11*2^210)
- // m = [666643, 470296, 654183, -997805, 136657, -683901] in 21-bit limbs
- s11 += s23 * 666643;
- s12 += s23 * 470296;
- s13 += s23 * 654183;
- s14 -= s23 * 997805;
- s15 += s23 * 136657;
- s16 -= s23 * 683901;
- // s23 = 0;
-
- s10 += s22 * 666643;
- s11 += s22 * 470296;
- s12 += s22 * 654183;
- s13 -= s22 * 997805;
- s14 += s22 * 136657;
- s15 -= s22 * 683901;
- // s22 = 0;
-
- s9 += s21 * 666643;
- s10 += s21 * 470296;
- s11 += s21 * 654183;
- s12 -= s21 * 997805;
- s13 += s21 * 136657;
- s14 -= s21 * 683901;
- // s21 = 0;
-
- s8 += s20 * 666643;
- s9 += s20 * 470296;
- s10 += s20 * 654183;
- s11 -= s20 * 997805;
- s12 += s20 * 136657;
- s13 -= s20 * 683901;
- // s20 = 0;
-
- s7 += s19 * 666643;
- s8 += s19 * 470296;
- s9 += s19 * 654183;
- s10 -= s19 * 997805;
- s11 += s19 * 136657;
- s12 -= s19 * 683901;
- // s19 = 0;
-
- s6 += s18 * 666643;
- s7 += s18 * 470296;
- s8 += s18 * 654183;
- s9 -= s18 * 997805;
- s10 += s18 * 136657;
- s11 -= s18 * 683901;
- // s18 = 0;
-
- // Reduce the bit length of limbs from s6 to s15 to 21-bits.
- carry6 = (s6 + (1 << 20)) >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry8 = (s8 + (1 << 20)) >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry10 = (s10 + (1 << 20)) >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
- carry12 = (s12 + (1 << 20)) >> 21;
- s13 += carry12;
- s12 -= carry12 << 21;
- carry14 = (s14 + (1 << 20)) >> 21;
- s15 += carry14;
- s14 -= carry14 << 21;
- carry16 = (s16 + (1 << 20)) >> 21;
- s17 += carry16;
- s16 -= carry16 << 21;
-
- carry7 = (s7 + (1 << 20)) >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry9 = (s9 + (1 << 20)) >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry11 = (s11 + (1 << 20)) >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
- carry13 = (s13 + (1 << 20)) >> 21;
- s14 += carry13;
- s13 -= carry13 << 21;
- carry15 = (s15 + (1 << 20)) >> 21;
- s16 += carry15;
- s15 -= carry15 << 21;
-
- // Resume reduction where we left off.
- s5 += s17 * 666643;
- s6 += s17 * 470296;
- s7 += s17 * 654183;
- s8 -= s17 * 997805;
- s9 += s17 * 136657;
- s10 -= s17 * 683901;
- // s17 = 0;
-
- s4 += s16 * 666643;
- s5 += s16 * 470296;
- s6 += s16 * 654183;
- s7 -= s16 * 997805;
- s8 += s16 * 136657;
- s9 -= s16 * 683901;
- // s16 = 0;
-
- s3 += s15 * 666643;
- s4 += s15 * 470296;
- s5 += s15 * 654183;
- s6 -= s15 * 997805;
- s7 += s15 * 136657;
- s8 -= s15 * 683901;
- // s15 = 0;
-
- s2 += s14 * 666643;
- s3 += s14 * 470296;
- s4 += s14 * 654183;
- s5 -= s14 * 997805;
- s6 += s14 * 136657;
- s7 -= s14 * 683901;
- // s14 = 0;
-
- s1 += s13 * 666643;
- s2 += s13 * 470296;
- s3 += s13 * 654183;
- s4 -= s13 * 997805;
- s5 += s13 * 136657;
- s6 -= s13 * 683901;
- // s13 = 0;
-
- s0 += s12 * 666643;
- s1 += s12 * 470296;
- s2 += s12 * 654183;
- s3 -= s12 * 997805;
- s4 += s12 * 136657;
- s5 -= s12 * 683901;
- s12 = 0;
-
- // Reduce the range of limbs from s0 to s11 to 21-bits.
- carry0 = (s0 + (1 << 20)) >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry2 = (s2 + (1 << 20)) >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry4 = (s4 + (1 << 20)) >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry6 = (s6 + (1 << 20)) >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry8 = (s8 + (1 << 20)) >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry10 = (s10 + (1 << 20)) >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
-
- carry1 = (s1 + (1 << 20)) >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry3 = (s3 + (1 << 20)) >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry5 = (s5 + (1 << 20)) >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry7 = (s7 + (1 << 20)) >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry9 = (s9 + (1 << 20)) >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry11 = (s11 + (1 << 20)) >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
-
- s0 += s12 * 666643;
- s1 += s12 * 470296;
- s2 += s12 * 654183;
- s3 -= s12 * 997805;
- s4 += s12 * 136657;
- s5 -= s12 * 683901;
- s12 = 0;
-
- // Carry chain reduction to propagate excess bits from s0 to s5 to the most significant limbs.
- carry0 = s0 >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry1 = s1 >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry2 = s2 >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry3 = s3 >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry4 = s4 >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry5 = s5 >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry6 = s6 >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry7 = s7 >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry8 = s8 >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry9 = s9 >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry10 = s10 >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
- carry11 = s11 >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
-
- // Do one last reduction as s12 might be 1.
- s0 += s12 * 666643;
- s1 += s12 * 470296;
- s2 += s12 * 654183;
- s3 -= s12 * 997805;
- s4 += s12 * 136657;
- s5 -= s12 * 683901;
- // s12 = 0;
-
- carry0 = s0 >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry1 = s1 >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry2 = s2 >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry3 = s3 >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry4 = s4 >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry5 = s5 >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry6 = s6 >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry7 = s7 >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry8 = s8 >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry9 = s9 >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry10 = s10 >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
-
- // Serialize the result into the s.
- s[0] = (byte) s0;
- s[1] = (byte) (s0 >> 8);
- s[2] = (byte) ((s0 >> 16) | (s1 << 5));
- s[3] = (byte) (s1 >> 3);
- s[4] = (byte) (s1 >> 11);
- s[5] = (byte) ((s1 >> 19) | (s2 << 2));
- s[6] = (byte) (s2 >> 6);
- s[7] = (byte) ((s2 >> 14) | (s3 << 7));
- s[8] = (byte) (s3 >> 1);
- s[9] = (byte) (s3 >> 9);
- s[10] = (byte) ((s3 >> 17) | (s4 << 4));
- s[11] = (byte) (s4 >> 4);
- s[12] = (byte) (s4 >> 12);
- s[13] = (byte) ((s4 >> 20) | (s5 << 1));
- s[14] = (byte) (s5 >> 7);
- s[15] = (byte) ((s5 >> 15) | (s6 << 6));
- s[16] = (byte) (s6 >> 2);
- s[17] = (byte) (s6 >> 10);
- s[18] = (byte) ((s6 >> 18) | (s7 << 3));
- s[19] = (byte) (s7 >> 5);
- s[20] = (byte) (s7 >> 13);
- s[21] = (byte) s8;
- s[22] = (byte) (s8 >> 8);
- s[23] = (byte) ((s8 >> 16) | (s9 << 5));
- s[24] = (byte) (s9 >> 3);
- s[25] = (byte) (s9 >> 11);
- s[26] = (byte) ((s9 >> 19) | (s10 << 2));
- s[27] = (byte) (s10 >> 6);
- s[28] = (byte) ((s10 >> 14) | (s11 << 7));
- s[29] = (byte) (s11 >> 1);
- s[30] = (byte) (s11 >> 9);
- s[31] = (byte) (s11 >> 17);
- }
-
- /**
- * Input:
- * a[0]+256*a[1]+...+256^31*a[31] = a
- * b[0]+256*b[1]+...+256^31*b[31] = b
- * c[0]+256*c[1]+...+256^31*c[31] = c
- *
- * Output:
- * s[0]+256*s[1]+...+256^31*s[31] = (ab+c) mod l
- * where l = 2^252 + 27742317777372353535851937790883648493.
- */
- private static void mulAdd(byte[] s, byte[] a, byte[] b, byte[] c) {
- // This is very similar to Ed25519.reduce, the difference in here is that it computes ab+c
- // See Ed25519.reduce for related comments.
- long a0 = 2097151 & load3(a, 0);
- long a1 = 2097151 & (load4(a, 2) >> 5);
- long a2 = 2097151 & (load3(a, 5) >> 2);
- long a3 = 2097151 & (load4(a, 7) >> 7);
- long a4 = 2097151 & (load4(a, 10) >> 4);
- long a5 = 2097151 & (load3(a, 13) >> 1);
- long a6 = 2097151 & (load4(a, 15) >> 6);
- long a7 = 2097151 & (load3(a, 18) >> 3);
- long a8 = 2097151 & load3(a, 21);
- long a9 = 2097151 & (load4(a, 23) >> 5);
- long a10 = 2097151 & (load3(a, 26) >> 2);
- long a11 = (load4(a, 28) >> 7);
- long b0 = 2097151 & load3(b, 0);
- long b1 = 2097151 & (load4(b, 2) >> 5);
- long b2 = 2097151 & (load3(b, 5) >> 2);
- long b3 = 2097151 & (load4(b, 7) >> 7);
- long b4 = 2097151 & (load4(b, 10) >> 4);
- long b5 = 2097151 & (load3(b, 13) >> 1);
- long b6 = 2097151 & (load4(b, 15) >> 6);
- long b7 = 2097151 & (load3(b, 18) >> 3);
- long b8 = 2097151 & load3(b, 21);
- long b9 = 2097151 & (load4(b, 23) >> 5);
- long b10 = 2097151 & (load3(b, 26) >> 2);
- long b11 = (load4(b, 28) >> 7);
- long c0 = 2097151 & load3(c, 0);
- long c1 = 2097151 & (load4(c, 2) >> 5);
- long c2 = 2097151 & (load3(c, 5) >> 2);
- long c3 = 2097151 & (load4(c, 7) >> 7);
- long c4 = 2097151 & (load4(c, 10) >> 4);
- long c5 = 2097151 & (load3(c, 13) >> 1);
- long c6 = 2097151 & (load4(c, 15) >> 6);
- long c7 = 2097151 & (load3(c, 18) >> 3);
- long c8 = 2097151 & load3(c, 21);
- long c9 = 2097151 & (load4(c, 23) >> 5);
- long c10 = 2097151 & (load3(c, 26) >> 2);
- long c11 = (load4(c, 28) >> 7);
- long s0;
- long s1;
- long s2;
- long s3;
- long s4;
- long s5;
- long s6;
- long s7;
- long s8;
- long s9;
- long s10;
- long s11;
- long s12;
- long s13;
- long s14;
- long s15;
- long s16;
- long s17;
- long s18;
- long s19;
- long s20;
- long s21;
- long s22;
- long s23;
- long carry0;
- long carry1;
- long carry2;
- long carry3;
- long carry4;
- long carry5;
- long carry6;
- long carry7;
- long carry8;
- long carry9;
- long carry10;
- long carry11;
- long carry12;
- long carry13;
- long carry14;
- long carry15;
- long carry16;
- long carry17;
- long carry18;
- long carry19;
- long carry20;
- long carry21;
- long carry22;
-
- s0 = c0 + a0 * b0;
- s1 = c1 + a0 * b1 + a1 * b0;
- s2 = c2 + a0 * b2 + a1 * b1 + a2 * b0;
- s3 = c3 + a0 * b3 + a1 * b2 + a2 * b1 + a3 * b0;
- s4 = c4 + a0 * b4 + a1 * b3 + a2 * b2 + a3 * b1 + a4 * b0;
- s5 = c5 + a0 * b5 + a1 * b4 + a2 * b3 + a3 * b2 + a4 * b1 + a5 * b0;
- s6 = c6 + a0 * b6 + a1 * b5 + a2 * b4 + a3 * b3 + a4 * b2 + a5 * b1 + a6 * b0;
- s7 = c7 + a0 * b7 + a1 * b6 + a2 * b5 + a3 * b4 + a4 * b3 + a5 * b2 + a6 * b1 + a7 * b0;
- s8 = c8 + a0 * b8 + a1 * b7 + a2 * b6 + a3 * b5 + a4 * b4 + a5 * b3 + a6 * b2 + a7 * b1
- + a8 * b0;
- s9 = c9 + a0 * b9 + a1 * b8 + a2 * b7 + a3 * b6 + a4 * b5 + a5 * b4 + a6 * b3 + a7 * b2
- + a8 * b1 + a9 * b0;
- s10 = c10 + a0 * b10 + a1 * b9 + a2 * b8 + a3 * b7 + a4 * b6 + a5 * b5 + a6 * b4 + a7 * b3
- + a8 * b2 + a9 * b1 + a10 * b0;
- s11 = c11 + a0 * b11 + a1 * b10 + a2 * b9 + a3 * b8 + a4 * b7 + a5 * b6 + a6 * b5 + a7 * b4
- + a8 * b3 + a9 * b2 + a10 * b1 + a11 * b0;
- s12 = a1 * b11 + a2 * b10 + a3 * b9 + a4 * b8 + a5 * b7 + a6 * b6 + a7 * b5 + a8 * b4 + a9 * b3
- + a10 * b2 + a11 * b1;
- s13 = a2 * b11 + a3 * b10 + a4 * b9 + a5 * b8 + a6 * b7 + a7 * b6 + a8 * b5 + a9 * b4 + a10 * b3
- + a11 * b2;
- s14 = a3 * b11 + a4 * b10 + a5 * b9 + a6 * b8 + a7 * b7 + a8 * b6 + a9 * b5 + a10 * b4
- + a11 * b3;
- s15 = a4 * b11 + a5 * b10 + a6 * b9 + a7 * b8 + a8 * b7 + a9 * b6 + a10 * b5 + a11 * b4;
- s16 = a5 * b11 + a6 * b10 + a7 * b9 + a8 * b8 + a9 * b7 + a10 * b6 + a11 * b5;
- s17 = a6 * b11 + a7 * b10 + a8 * b9 + a9 * b8 + a10 * b7 + a11 * b6;
- s18 = a7 * b11 + a8 * b10 + a9 * b9 + a10 * b8 + a11 * b7;
- s19 = a8 * b11 + a9 * b10 + a10 * b9 + a11 * b8;
- s20 = a9 * b11 + a10 * b10 + a11 * b9;
- s21 = a10 * b11 + a11 * b10;
- s22 = a11 * b11;
- s23 = 0;
-
- carry0 = (s0 + (1 << 20)) >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry2 = (s2 + (1 << 20)) >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry4 = (s4 + (1 << 20)) >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry6 = (s6 + (1 << 20)) >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry8 = (s8 + (1 << 20)) >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry10 = (s10 + (1 << 20)) >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
- carry12 = (s12 + (1 << 20)) >> 21;
- s13 += carry12;
- s12 -= carry12 << 21;
- carry14 = (s14 + (1 << 20)) >> 21;
- s15 += carry14;
- s14 -= carry14 << 21;
- carry16 = (s16 + (1 << 20)) >> 21;
- s17 += carry16;
- s16 -= carry16 << 21;
- carry18 = (s18 + (1 << 20)) >> 21;
- s19 += carry18;
- s18 -= carry18 << 21;
- carry20 = (s20 + (1 << 20)) >> 21;
- s21 += carry20;
- s20 -= carry20 << 21;
- carry22 = (s22 + (1 << 20)) >> 21;
- s23 += carry22;
- s22 -= carry22 << 21;
-
- carry1 = (s1 + (1 << 20)) >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry3 = (s3 + (1 << 20)) >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry5 = (s5 + (1 << 20)) >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry7 = (s7 + (1 << 20)) >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry9 = (s9 + (1 << 20)) >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry11 = (s11 + (1 << 20)) >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
- carry13 = (s13 + (1 << 20)) >> 21;
- s14 += carry13;
- s13 -= carry13 << 21;
- carry15 = (s15 + (1 << 20)) >> 21;
- s16 += carry15;
- s15 -= carry15 << 21;
- carry17 = (s17 + (1 << 20)) >> 21;
- s18 += carry17;
- s17 -= carry17 << 21;
- carry19 = (s19 + (1 << 20)) >> 21;
- s20 += carry19;
- s19 -= carry19 << 21;
- carry21 = (s21 + (1 << 20)) >> 21;
- s22 += carry21;
- s21 -= carry21 << 21;
-
- s11 += s23 * 666643;
- s12 += s23 * 470296;
- s13 += s23 * 654183;
- s14 -= s23 * 997805;
- s15 += s23 * 136657;
- s16 -= s23 * 683901;
- // s23 = 0;
-
- s10 += s22 * 666643;
- s11 += s22 * 470296;
- s12 += s22 * 654183;
- s13 -= s22 * 997805;
- s14 += s22 * 136657;
- s15 -= s22 * 683901;
- // s22 = 0;
-
- s9 += s21 * 666643;
- s10 += s21 * 470296;
- s11 += s21 * 654183;
- s12 -= s21 * 997805;
- s13 += s21 * 136657;
- s14 -= s21 * 683901;
- // s21 = 0;
-
- s8 += s20 * 666643;
- s9 += s20 * 470296;
- s10 += s20 * 654183;
- s11 -= s20 * 997805;
- s12 += s20 * 136657;
- s13 -= s20 * 683901;
- // s20 = 0;
-
- s7 += s19 * 666643;
- s8 += s19 * 470296;
- s9 += s19 * 654183;
- s10 -= s19 * 997805;
- s11 += s19 * 136657;
- s12 -= s19 * 683901;
- // s19 = 0;
-
- s6 += s18 * 666643;
- s7 += s18 * 470296;
- s8 += s18 * 654183;
- s9 -= s18 * 997805;
- s10 += s18 * 136657;
- s11 -= s18 * 683901;
- // s18 = 0;
-
- carry6 = (s6 + (1 << 20)) >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry8 = (s8 + (1 << 20)) >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry10 = (s10 + (1 << 20)) >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
- carry12 = (s12 + (1 << 20)) >> 21;
- s13 += carry12;
- s12 -= carry12 << 21;
- carry14 = (s14 + (1 << 20)) >> 21;
- s15 += carry14;
- s14 -= carry14 << 21;
- carry16 = (s16 + (1 << 20)) >> 21;
- s17 += carry16;
- s16 -= carry16 << 21;
-
- carry7 = (s7 + (1 << 20)) >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry9 = (s9 + (1 << 20)) >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry11 = (s11 + (1 << 20)) >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
- carry13 = (s13 + (1 << 20)) >> 21;
- s14 += carry13;
- s13 -= carry13 << 21;
- carry15 = (s15 + (1 << 20)) >> 21;
- s16 += carry15;
- s15 -= carry15 << 21;
-
- s5 += s17 * 666643;
- s6 += s17 * 470296;
- s7 += s17 * 654183;
- s8 -= s17 * 997805;
- s9 += s17 * 136657;
- s10 -= s17 * 683901;
- // s17 = 0;
-
- s4 += s16 * 666643;
- s5 += s16 * 470296;
- s6 += s16 * 654183;
- s7 -= s16 * 997805;
- s8 += s16 * 136657;
- s9 -= s16 * 683901;
- // s16 = 0;
-
- s3 += s15 * 666643;
- s4 += s15 * 470296;
- s5 += s15 * 654183;
- s6 -= s15 * 997805;
- s7 += s15 * 136657;
- s8 -= s15 * 683901;
- // s15 = 0;
-
- s2 += s14 * 666643;
- s3 += s14 * 470296;
- s4 += s14 * 654183;
- s5 -= s14 * 997805;
- s6 += s14 * 136657;
- s7 -= s14 * 683901;
- // s14 = 0;
-
- s1 += s13 * 666643;
- s2 += s13 * 470296;
- s3 += s13 * 654183;
- s4 -= s13 * 997805;
- s5 += s13 * 136657;
- s6 -= s13 * 683901;
- // s13 = 0;
-
- s0 += s12 * 666643;
- s1 += s12 * 470296;
- s2 += s12 * 654183;
- s3 -= s12 * 997805;
- s4 += s12 * 136657;
- s5 -= s12 * 683901;
- s12 = 0;
-
- carry0 = (s0 + (1 << 20)) >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry2 = (s2 + (1 << 20)) >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry4 = (s4 + (1 << 20)) >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry6 = (s6 + (1 << 20)) >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry8 = (s8 + (1 << 20)) >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry10 = (s10 + (1 << 20)) >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
-
- carry1 = (s1 + (1 << 20)) >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry3 = (s3 + (1 << 20)) >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry5 = (s5 + (1 << 20)) >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry7 = (s7 + (1 << 20)) >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry9 = (s9 + (1 << 20)) >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry11 = (s11 + (1 << 20)) >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
-
- s0 += s12 * 666643;
- s1 += s12 * 470296;
- s2 += s12 * 654183;
- s3 -= s12 * 997805;
- s4 += s12 * 136657;
- s5 -= s12 * 683901;
- s12 = 0;
-
- carry0 = s0 >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry1 = s1 >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry2 = s2 >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry3 = s3 >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry4 = s4 >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry5 = s5 >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry6 = s6 >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry7 = s7 >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry8 = s8 >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry9 = s9 >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry10 = s10 >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
- carry11 = s11 >> 21;
- s12 += carry11;
- s11 -= carry11 << 21;
-
- s0 += s12 * 666643;
- s1 += s12 * 470296;
- s2 += s12 * 654183;
- s3 -= s12 * 997805;
- s4 += s12 * 136657;
- s5 -= s12 * 683901;
- // s12 = 0;
-
- carry0 = s0 >> 21;
- s1 += carry0;
- s0 -= carry0 << 21;
- carry1 = s1 >> 21;
- s2 += carry1;
- s1 -= carry1 << 21;
- carry2 = s2 >> 21;
- s3 += carry2;
- s2 -= carry2 << 21;
- carry3 = s3 >> 21;
- s4 += carry3;
- s3 -= carry3 << 21;
- carry4 = s4 >> 21;
- s5 += carry4;
- s4 -= carry4 << 21;
- carry5 = s5 >> 21;
- s6 += carry5;
- s5 -= carry5 << 21;
- carry6 = s6 >> 21;
- s7 += carry6;
- s6 -= carry6 << 21;
- carry7 = s7 >> 21;
- s8 += carry7;
- s7 -= carry7 << 21;
- carry8 = s8 >> 21;
- s9 += carry8;
- s8 -= carry8 << 21;
- carry9 = s9 >> 21;
- s10 += carry9;
- s9 -= carry9 << 21;
- carry10 = s10 >> 21;
- s11 += carry10;
- s10 -= carry10 << 21;
-
- s[0] = (byte) s0;
- s[1] = (byte) (s0 >> 8);
- s[2] = (byte) ((s0 >> 16) | (s1 << 5));
- s[3] = (byte) (s1 >> 3);
- s[4] = (byte) (s1 >> 11);
- s[5] = (byte) ((s1 >> 19) | (s2 << 2));
- s[6] = (byte) (s2 >> 6);
- s[7] = (byte) ((s2 >> 14) | (s3 << 7));
- s[8] = (byte) (s3 >> 1);
- s[9] = (byte) (s3 >> 9);
- s[10] = (byte) ((s3 >> 17) | (s4 << 4));
- s[11] = (byte) (s4 >> 4);
- s[12] = (byte) (s4 >> 12);
- s[13] = (byte) ((s4 >> 20) | (s5 << 1));
- s[14] = (byte) (s5 >> 7);
- s[15] = (byte) ((s5 >> 15) | (s6 << 6));
- s[16] = (byte) (s6 >> 2);
- s[17] = (byte) (s6 >> 10);
- s[18] = (byte) ((s6 >> 18) | (s7 << 3));
- s[19] = (byte) (s7 >> 5);
- s[20] = (byte) (s7 >> 13);
- s[21] = (byte) s8;
- s[22] = (byte) (s8 >> 8);
- s[23] = (byte) ((s8 >> 16) | (s9 << 5));
- s[24] = (byte) (s9 >> 3);
- s[25] = (byte) (s9 >> 11);
- s[26] = (byte) ((s9 >> 19) | (s10 << 2));
- s[27] = (byte) (s10 >> 6);
- s[28] = (byte) ((s10 >> 14) | (s11 << 7));
- s[29] = (byte) (s11 >> 1);
- s[30] = (byte) (s11 >> 9);
- s[31] = (byte) (s11 >> 17);
- }
-
- // The order of the generator as unsigned bytes in little endian order.
- // (2^252 + 0x14def9dea2f79cd65812631a5cf5d3ed, cf. RFC 7748)
- private static final byte[] GROUP_ORDER = {
- (byte) 0xed, (byte) 0xd3, (byte) 0xf5, (byte) 0x5c,
- (byte) 0x1a, (byte) 0x63, (byte) 0x12, (byte) 0x58,
- (byte) 0xd6, (byte) 0x9c, (byte) 0xf7, (byte) 0xa2,
- (byte) 0xde, (byte) 0xf9, (byte) 0xde, (byte) 0x14,
- (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
- (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
- (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x00,
- (byte) 0x00, (byte) 0x00, (byte) 0x00, (byte) 0x10};
-
- // Checks whether s represents an integer smaller than the order of the group.
- // This is needed to ensure that EdDSA signatures are non-malleable, as failing to check
- // the range of S allows to modify signatures (cf. RFC 8032, Section 5.2.7 and Section 8.4.)
- // @param s an integer in little-endian order.
- private static boolean isSmallerThanGroupOrder(byte[] s) {
- for (int j = Field25519.FIELD_LEN - 1; j >= 0; j--) {
- // compare unsigned bytes
- int a = s[j] & 0xff;
- int b = GROUP_ORDER[j] & 0xff;
- if (a != b) {
- return a < b;
- }
- }
- return false;
- }
-
- /**
- * Returns true if the EdDSA {@code signature} with {@code message}, can be verified with
- * {@code publicKey}.
- */
- public static boolean verify(final byte[] message, final byte[] signature,
- final byte[] publicKey) {
- try {
- if (signature.length != SIGNATURE_LEN) {
- return false;
- }
- if (publicKey.length != PUBLIC_KEY_LEN) {
- return false;
- }
- byte[] s = Arrays.copyOfRange(signature, Field25519.FIELD_LEN, SIGNATURE_LEN);
- if (!isSmallerThanGroupOrder(s)) {
- return false;
- }
- MessageDigest digest = MessageDigest.getInstance("SHA-512");
- digest.update(signature, 0, Field25519.FIELD_LEN);
- digest.update(publicKey);
- digest.update(message);
- byte[] h = digest.digest();
- reduce(h);
-
- XYZT negPublicKey = XYZT.fromBytesNegateVarTime(publicKey);
- XYZ xyz = doubleScalarMultVarTime(h, negPublicKey, s);
- byte[] expectedR = xyz.toBytes();
- for (int i = 0; i < Field25519.FIELD_LEN; i++) {
- if (expectedR[i] != signature[i]) {
- return false;
- }
- }
- return true;
- } catch (final GeneralSecurityException ignored) {
- return false;
- }
- }
-}
diff --git a/app/src/main/java/com/wireguard/crypto/Key.java b/app/src/main/java/com/wireguard/crypto/Key.java
deleted file mode 100644
index 9e25e6057..000000000
--- a/app/src/main/java/com/wireguard/crypto/Key.java
+++ /dev/null
@@ -1,288 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.crypto;
-
-import com.wireguard.crypto.KeyFormatException.Type;
-
-import java.security.MessageDigest;
-import java.security.SecureRandom;
-import java.util.Arrays;
-
-/**
- * Represents a WireGuard public or private key. This class uses specialized constant-time base64
- * and hexadecimal codec implementations that resist side-channel attacks.
- *
- * Instances of this class are immutable.
- */
-@SuppressWarnings("MagicNumber")
-public final class Key {
- private final byte[] key;
-
- /**
- * Constructs an object encapsulating the supplied key.
- *
- * @param key an array of bytes containing a binary key. Callers of this constructor are
- * responsible for ensuring that the array is of the correct length.
- */
- private Key(final byte[] key) {
- // Defensively copy to ensure immutability.
- this.key = Arrays.copyOf(key, key.length);
- }
-
- /**
- * Decodes a single 4-character base64 chunk to an integer in constant time.
- *
- * @param src an array of at least 4 characters in base64 format
- * @param srcOffset the offset of the beginning of the chunk in {@code src}
- * @return the decoded 3-byte integer, or some arbitrary integer value if the input was not
- * valid base64
- */
- private static int decodeBase64(final char[] src, final int srcOffset) {
- int val = 0;
- for (int i = 0; i < 4; ++i) {
- final char c = src[i + srcOffset];
- val |= (-1
- + ((((('A' - 1) - c) & (c - ('Z' + 1))) >>> 8) & (c - 64))
- + ((((('a' - 1) - c) & (c - ('z' + 1))) >>> 8) & (c - 70))
- + ((((('0' - 1) - c) & (c - ('9' + 1))) >>> 8) & (c + 5))
- + ((((('+' - 1) - c) & (c - ('+' + 1))) >>> 8) & 63)
- + ((((('/' - 1) - c) & (c - ('/' + 1))) >>> 8) & 64)
- ) << (18 - 6 * i);
- }
- return val;
- }
-
- /**
- * Encodes a single 4-character base64 chunk from 3 consecutive bytes in constant time.
- *
- * @param src an array of at least 3 bytes
- * @param srcOffset the offset of the beginning of the chunk in {@code src}
- * @param dest an array of at least 4 characters
- * @param destOffset the offset of the beginning of the chunk in {@code dest}
- */
- private static void encodeBase64(final byte[] src, final int srcOffset,
- final char[] dest, final int destOffset) {
- final byte[] input = {
- (byte) ((src[srcOffset] >>> 2) & 63),
- (byte) ((src[srcOffset] << 4 | ((src[1 + srcOffset] & 0xff) >>> 4)) & 63),
- (byte) ((src[1 + srcOffset] << 2 | ((src[2 + srcOffset] & 0xff) >>> 6)) & 63),
- (byte) ((src[2 + srcOffset]) & 63),
- };
- for (int i = 0; i < 4; ++i) {
- dest[i + destOffset] = (char) (input[i] + 'A'
- + (((25 - input[i]) >>> 8) & 6)
- - (((51 - input[i]) >>> 8) & 75)
- - (((61 - input[i]) >>> 8) & 15)
- + (((62 - input[i]) >>> 8) & 3));
- }
- }
-
- /**
- * Decodes a WireGuard public or private key from its base64 string representation. This
- * function throws a {@link KeyFormatException} if the source string is not well-formed.
- *
- * @param str the base64 string representation of a WireGuard key
- * @return the decoded key encapsulated in an immutable container
- */
- public static Key fromBase64(final String str) throws KeyFormatException {
- final char[] input = str.toCharArray();
- if (input.length != Format.BASE64.length || input[Format.BASE64.length - 1] != '=')
- throw new KeyFormatException(Format.BASE64, Type.LENGTH);
- final byte[] key = new byte[Format.BINARY.length];
- int i;
- int ret = 0;
- for (i = 0; i < key.length / 3; ++i) {
- final int val = decodeBase64(input, i * 4);
- ret |= val >>> 31;
- key[i * 3] = (byte) ((val >>> 16) & 0xff);
- key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
- key[i * 3 + 2] = (byte) (val & 0xff);
- }
- final char[] endSegment = {
- input[i * 4],
- input[i * 4 + 1],
- input[i * 4 + 2],
- 'A',
- };
- final int val = decodeBase64(endSegment, 0);
- ret |= (val >>> 31) | (val & 0xff);
- key[i * 3] = (byte) ((val >>> 16) & 0xff);
- key[i * 3 + 1] = (byte) ((val >>> 8) & 0xff);
-
- if (ret != 0)
- throw new KeyFormatException(Format.BASE64, Type.CONTENTS);
- return new Key(key);
- }
-
- /**
- * Wraps a WireGuard public or private key in an immutable container. This function throws a
- * {@link KeyFormatException} if the source data is not the correct length.
- *
- * @param bytes an array of bytes containing a WireGuard key in binary format
- * @return the key encapsulated in an immutable container
- */
- public static Key fromBytes(final byte[] bytes) throws KeyFormatException {
- if (bytes.length != Format.BINARY.length)
- throw new KeyFormatException(Format.BINARY, Type.LENGTH);
- return new Key(bytes);
- }
-
- /**
- * Decodes a WireGuard public or private key from its hexadecimal string representation. This
- * function throws a {@link KeyFormatException} if the source string is not well-formed.
- *
- * @param str the hexadecimal string representation of a WireGuard key
- * @return the decoded key encapsulated in an immutable container
- */
- public static Key fromHex(final String str) throws KeyFormatException {
- final char[] input = str.toCharArray();
- if (input.length != Format.HEX.length)
- throw new KeyFormatException(Format.HEX, Type.LENGTH);
- final byte[] key = new byte[Format.BINARY.length];
- int ret = 0;
- for (int i = 0; i < key.length; ++i) {
- int c;
- int cNum;
- int cNum0;
- int cAlpha;
- int cAlpha0;
- int cVal;
- final int cAcc;
-
- c = input[i * 2];
- cNum = c ^ 48;
- cNum0 = ((cNum - 10) >>> 8) & 0xff;
- cAlpha = (c & ~32) - 55;
- cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
- ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
- cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
- cAcc = cVal * 16;
-
- c = input[i * 2 + 1];
- cNum = c ^ 48;
- cNum0 = ((cNum - 10) >>> 8) & 0xff;
- cAlpha = (c & ~32) - 55;
- cAlpha0 = (((cAlpha - 10) ^ (cAlpha - 16)) >>> 8) & 0xff;
- ret |= ((cNum0 | cAlpha0) - 1) >>> 8;
- cVal = (cNum0 & cNum) | (cAlpha0 & cAlpha);
- key[i] = (byte) (cAcc | cVal);
- }
- if (ret != 0)
- throw new KeyFormatException(Format.HEX, Type.CONTENTS);
- return new Key(key);
- }
-
- /**
- * Generates a private key using the system's {@link SecureRandom} number generator.
- *
- * @return a well-formed random private key
- */
- static Key generatePrivateKey() {
- final SecureRandom secureRandom = new SecureRandom();
- final byte[] privateKey = new byte[Format.BINARY.getLength()];
- secureRandom.nextBytes(privateKey);
- privateKey[0] &= 248;
- privateKey[31] &= 127;
- privateKey[31] |= 64;
- return new Key(privateKey);
- }
-
- /**
- * Generates a public key from an existing private key.
- *
- * @param privateKey a private key
- * @return a well-formed public key that corresponds to the supplied private key
- */
- static Key generatePublicKey(final Key privateKey) {
- final byte[] publicKey = new byte[Format.BINARY.getLength()];
- Curve25519.eval(publicKey, 0, privateKey.getBytes(), null);
- return new Key(publicKey);
- }
-
- @Override
- public boolean equals(final Object obj) {
- if (obj == this)
- return true;
- if (obj == null || obj.getClass() != getClass())
- return false;
- final Key other = (Key) obj;
- return MessageDigest.isEqual(key, other.key);
- }
-
- /**
- * Returns the key as an array of bytes.
- *
- * @return an array of bytes containing the raw binary key
- */
- public byte[] getBytes() {
- // Defensively copy to ensure immutability.
- return Arrays.copyOf(key, key.length);
- }
-
- @Override
- public int hashCode() {
- int ret = 0;
- for (int i = 0; i < key.length / 4; ++i)
- ret ^= (key[i * 4 + 0] >> 0) + (key[i * 4 + 1] >> 8) + (key[i * 4 + 2] >> 16) + (key[i * 4 + 3] >> 24);
- return ret;
- }
-
- /**
- * Encodes the key to base64.
- *
- * @return a string containing the encoded key
- */
- public String toBase64() {
- final char[] output = new char[Format.BASE64.length];
- int i;
- for (i = 0; i < key.length / 3; ++i)
- encodeBase64(key, i * 3, output, i * 4);
- final byte[] endSegment = {
- key[i * 3],
- key[i * 3 + 1],
- 0,
- };
- encodeBase64(endSegment, 0, output, i * 4);
- output[Format.BASE64.length - 1] = '=';
- return new String(output);
- }
-
- /**
- * Encodes the key to hexadecimal ASCII characters.
- *
- * @return a string containing the encoded key
- */
- public String toHex() {
- final char[] output = new char[Format.HEX.length];
- for (int i = 0; i < key.length; ++i) {
- output[i * 2] = (char) (87 + (key[i] >> 4 & 0xf)
- + ((((key[i] >> 4 & 0xf) - 10) >> 8) & ~38));
- output[i * 2 + 1] = (char) (87 + (key[i] & 0xf)
- + ((((key[i] & 0xf) - 10) >> 8) & ~38));
- }
- return new String(output);
- }
-
- /**
- * The supported formats for encoding a WireGuard key.
- */
- public enum Format {
- BASE64(44),
- BINARY(32),
- HEX(64);
-
- private final int length;
-
- Format(final int length) {
- this.length = length;
- }
-
- public int getLength() {
- return length;
- }
- }
-
-}
diff --git a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java b/app/src/main/java/com/wireguard/crypto/KeyFormatException.java
deleted file mode 100644
index 5818b4d45..000000000
--- a/app/src/main/java/com/wireguard/crypto/KeyFormatException.java
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright © 2018-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.crypto;
-
-/**
- * An exception thrown when attempting to parse an invalid key (too short, too long, or byte
- * data inappropriate for the format). The format being parsed can be accessed with the
- * {@link #getFormat} method.
- */
-public final class KeyFormatException extends Exception {
- private final Key.Format format;
- private final Type type;
-
- KeyFormatException(final Key.Format format, final Type type) {
- this.format = format;
- this.type = type;
- }
-
- public Key.Format getFormat() {
- return format;
- }
-
- public Type getType() {
- return type;
- }
-
- public enum Type {
- CONTENTS,
- LENGTH
- }
-}
diff --git a/app/src/main/java/com/wireguard/crypto/KeyPair.java b/app/src/main/java/com/wireguard/crypto/KeyPair.java
deleted file mode 100644
index f8238e91c..000000000
--- a/app/src/main/java/com/wireguard/crypto/KeyPair.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright © 2017-2019 WireGuard LLC. All Rights Reserved.
- * SPDX-License-Identifier: Apache-2.0
- */
-
-package com.wireguard.crypto;
-
-/**
- * Represents a Curve25519 key pair as used by WireGuard.
- *
- * Instances of this class are immutable.
- */
-public class KeyPair {
- private final Key privateKey;
- private final Key publicKey;
-
- /**
- * Creates a key pair using a newly-generated private key.
- */
- public KeyPair() {
- this(Key.generatePrivateKey());
- }
-
- /**
- * Creates a key pair using an existing private key.
- *
- * @param privateKey a private key, used to derive the public key
- */
- public KeyPair(final Key privateKey) {
- this.privateKey = privateKey;
- publicKey = Key.generatePublicKey(privateKey);
- }
-
- /**
- * Returns the private key from the key pair.
- *
- * @return the private key
- */
- public Key getPrivateKey() {
- return privateKey;
- }
-
- /**
- * Returns the public key from the key pair.
- *
- * @return the public key
- */
- public Key getPublicKey() {
- return publicKey;
- }
-}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
index d44a3b601..caf363f6f 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
@@ -16,6 +16,8 @@ object Key {
const val MODE_VPN = "vpn"
const val MODE_PROXY = "proxy"
+ const val GLOBAL_CUSTOM_CONFIG = "globalCustomConfig"
+
const val REMOTE_DNS = "remoteDns"
const val DIRECT_DNS = "directDns"
const val ENABLE_DNS_ROUTING = "enableDnsRouting"
diff --git a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
index 2cc881b40..f38305d46 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/SagerNet.kt
@@ -20,6 +20,8 @@ import go.Seq
import io.nekohasekai.sagernet.bg.SagerConnection
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.ktx.Logs
+import io.nekohasekai.sagernet.ktx.isOss
+import io.nekohasekai.sagernet.ktx.isPreview
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import io.nekohasekai.sagernet.ui.MainActivity
import io.nekohasekai.sagernet.utils.*
@@ -27,6 +29,7 @@ import kotlinx.coroutines.DEBUG_PROPERTY_NAME
import kotlinx.coroutines.DEBUG_PROPERTY_VALUE_ON
import libcore.Libcore
import moe.matsuri.nb4a.NativeInterface
+import moe.matsuri.nb4a.net.LocalResolverImpl
import moe.matsuri.nb4a.utils.JavaUtil
import moe.matsuri.nb4a.utils.cleanWebview
import java.io.File
@@ -51,10 +54,21 @@ class SagerNet : Application(),
override fun onCreate() {
super.onCreate()
- System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
Thread.setDefaultUncaughtExceptionHandler(CrashHandler)
if (isMainProcess || isBgProcess) {
+ externalAssets.mkdirs()
+ Seq.setContext(this)
+ Libcore.initCore(
+ process,
+ cacheDir.absolutePath + "/",
+ filesDir.absolutePath + "/",
+ externalAssets.absolutePath + "/",
+ DataStore.logBufSize,
+ DataStore.logLevel > 0,
+ nativeInterface, nativeInterface, LocalResolverImpl
+ )
+
// fix multi process issue in Android 9+
JavaUtil.handleWebviewDir(this)
@@ -64,21 +78,6 @@ class SagerNet : Application(),
}
}
- Seq.setContext(this)
- updateNotificationChannels()
-
- // nb4a: init core
- externalAssets.mkdirs()
- Libcore.initCore(
- process,
- cacheDir.absolutePath + "/",
- filesDir.absolutePath + "/",
- externalAssets.absolutePath + "/",
- DataStore.logBufSize,
- DataStore.logLevel > 0,
- nativeInterface, nativeInterface
- )
-
if (isMainProcess) {
Theme.apply(this)
Theme.applyNightTheme()
@@ -86,17 +85,22 @@ class SagerNet : Application(),
DefaultNetworkListener.start(this) {
underlyingNetwork = it
}
+
+ updateNotificationChannels()
}
}
- if (BuildConfig.DEBUG) StrictMode.setVmPolicy(
- StrictMode.VmPolicy.Builder()
- .detectLeakedSqlLiteObjects()
- .detectLeakedClosableObjects()
- .detectLeakedRegistrationObjects()
- .penaltyLog()
- .build()
- )
+ if (BuildConfig.DEBUG) {
+ System.setProperty(DEBUG_PROPERTY_NAME, DEBUG_PROPERTY_VALUE_ON)
+ StrictMode.setVmPolicy(
+ StrictMode.VmPolicy.Builder()
+ .detectLeakedSqlLiteObjects()
+ .detectLeakedClosableObjects()
+ .detectLeakedRegistrationObjects()
+ .penaltyLog()
+ .build()
+ )
+ }
}
override fun onConfigurationChanged(newConfig: Configuration) {
@@ -176,6 +180,10 @@ class SagerNet : Application(),
"service-subscription",
application.getText(R.string.service_subscription),
NotificationManager.IMPORTANCE_DEFAULT
+ ), NotificationChannel(
+ "connection-test",
+ application.getText(R.string.connection_test),
+ NotificationManager.IMPORTANCE_DEFAULT
)
)
)
@@ -194,6 +202,18 @@ class SagerNet : Application(),
var underlyingNetwork: Network? = null
+ var appVersionNameForDisplay = {
+ var n = BuildConfig.VERSION_NAME
+ if (isPreview) {
+ n += " " + BuildConfig.PRE_VERSION_NAME
+ } else if (!isOss) {
+ n += " ${BuildConfig.FLAVOR}"
+ }
+ if (BuildConfig.DEBUG) {
+ n += " DEBUG"
+ }
+ n
+ }()
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
index 4d6950436..e760983dc 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
@@ -105,6 +105,10 @@ class BaseService {
override fun getProfileName(): String = data?.proxy?.displayProfileName ?: "Idle"
override fun registerCallback(cb: ISagerNetServiceCallback, id: Int) {
+ if (id == SagerConnection.CONNECTION_ID_RESTART_BG) {
+ Runtime.getRuntime().exit(0)
+ return
+ }
if (!callbackIdMap.contains(cb)) {
callbacks.register(cb)
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
index f944058dc..5b860b35f 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/Executable.kt
@@ -3,24 +3,21 @@ package io.nekohasekai.sagernet.bg
import android.system.ErrnoException
import android.system.Os
import android.system.OsConstants
-import android.text.TextUtils
import io.nekohasekai.sagernet.ktx.Logs
import java.io.File
import java.io.IOException
+import androidx.core.text.isDigitsOnly
object Executable {
private val EXECUTABLES = setOf(
- "libtrojan.so",
- "libtrojan-go.so",
- "libnaive.so",
- "libtuic.so",
- "libhysteria.so"
+ "libtrojan.so", "libtrojan-go.so", "libnaive.so", "libtuic.so", "libhysteria.so"
)
fun killAll(alsoKillBg: Boolean = false) {
- for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }
- ?: return) {
- val exe = File(try {
+ // kill bg may fail
+ for (process in File("/proc").listFiles { _, name -> name.isDigitsOnly() } ?: return) {
+ val exe = File(
+ try {
File(process, "cmdline").inputStream().bufferedReader().use {
it.readText()
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
index ab7d5a493..97ff4b8a0 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/SagerConnection.kt
@@ -32,6 +32,9 @@ class SagerConnection(
const val CONNECTION_ID_TILE = 1
const val CONNECTION_ID_MAIN_ACTIVITY_FOREGROUND = 2
const val CONNECTION_ID_MAIN_ACTIVITY_BACKGROUND = 3
+ const val CONNECTION_ID_RESTART_BG = 4
+
+ var restartingApp = false
}
interface Callback {
@@ -124,7 +127,7 @@ class SagerConnection(
} catch (e: RemoteException) {
e.printStackTrace()
}
- callback!!.onServiceConnected(service)
+ callback?.onServiceConnected(service)
}
override fun onServiceDisconnected(name: ComponentName?) {
@@ -137,7 +140,9 @@ class SagerConnection(
override fun binderDied() {
service = null
callbackRegistered = false
- callback?.also { runOnMainDispatcher { it.onBinderDied() } }
+ if (!restartingApp) {
+ callback?.also { runOnMainDispatcher { it.onBinderDied() } }
+ }
}
private fun unregisterCallback() {
@@ -149,7 +154,7 @@ class SagerConnection(
callbackRegistered = false
}
- fun connect(context: Context, callback: Callback) {
+ fun connect(context: Context, callback: Callback?) {
if (connectionActive) return
connectionActive = true
check(this.callback == null)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
index 84586bc2c..9f16723de 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/BoxInstance.kt
@@ -21,6 +21,7 @@ import io.nekohasekai.sagernet.plugin.PluginManager
import kotlinx.coroutines.*
import libcore.BoxInstance
import libcore.Libcore
+import moe.matsuri.nb4a.net.LocalResolverImpl
import java.io.File
abstract class BoxInstance(
@@ -48,7 +49,7 @@ abstract class BoxInstance(
}
protected open suspend fun loadConfig() {
- box = Libcore.newSingBoxInstance(config.config)
+ box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl)
}
open suspend fun init() {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
index 293c65742..9758a5c40 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/ProxyInstance.kt
@@ -7,8 +7,6 @@ import io.nekohasekai.sagernet.database.ProxyEntity
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.runOnDefaultDispatcher
import kotlinx.coroutines.runBlocking
-import libcore.Libcore
-import moe.matsuri.nb4a.net.LocalResolverImpl
import moe.matsuri.nb4a.utils.JavaUtil
class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? = null) :
@@ -45,7 +43,6 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? =
}
override suspend fun loadConfig() {
- Libcore.registerLocalDNSTransport(LocalResolverImpl)
super.loadConfig()
}
@@ -59,7 +56,6 @@ class ProxyInstance(profile: ProxyEntity, var service: BaseService.Interface? =
}
override fun close() {
- Libcore.registerLocalDNSTransport(null)
super.close()
runBlocking {
looper?.stop()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt
index 57d1a1702..bc6907137 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/proto/TestInstance.kt
@@ -10,6 +10,7 @@ import io.nekohasekai.sagernet.ktx.tryResume
import io.nekohasekai.sagernet.ktx.tryResumeWithException
import kotlinx.coroutines.delay
import libcore.Libcore
+import moe.matsuri.nb4a.net.LocalResolverImpl
import kotlin.coroutines.suspendCoroutine
class TestInstance(profile: ProxyEntity, val link: String, private val timeout: Int) :
@@ -46,7 +47,7 @@ class TestInstance(profile: ProxyEntity, val link: String, private val timeout:
override suspend fun loadConfig() {
// don't call destroyAllJsi here
if (BuildConfig.DEBUG) Logs.d(config.config)
- box = Libcore.newSingBoxInstance(config.config)
+ box = Libcore.newSingBoxInstance(config.config, LocalResolverImpl)
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
index df6f06ab4..06bb50133 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/DataStore.kt
@@ -17,7 +17,6 @@ import io.nekohasekai.sagernet.ktx.int
import io.nekohasekai.sagernet.ktx.long
import io.nekohasekai.sagernet.ktx.parsePort
import io.nekohasekai.sagernet.ktx.string
-import io.nekohasekai.sagernet.ktx.stringSet
import io.nekohasekai.sagernet.ktx.stringToInt
import io.nekohasekai.sagernet.ktx.stringToIntIfExists
import moe.matsuri.nb4a.TempDatabase
@@ -41,6 +40,10 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var vpnService: VpnService? = null
var baseService: BaseService.Interface? = null
+ // main
+
+ var runningTest = false
+
fun currentGroupId(): Long {
val currentSelected = configurationStore.getLong(Key.PROFILE_GROUP, -1)
if (currentSelected > 0L) return currentSelected
@@ -108,10 +111,12 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var speedInterval by configurationStore.stringToInt(Key.SPEED_INTERVAL)
var showGroupInNotification by configurationStore.boolean("showGroupInNotification")
+ var globalCustomConfig by configurationStore.string(Key.GLOBAL_CUSTOM_CONFIG) { "" }
+
var remoteDns by configurationStore.string(Key.REMOTE_DNS) { "https://dns.google/dns-query" }
var directDns by configurationStore.string(Key.DIRECT_DNS) { "https://223.5.5.5/dns-query" }
var enableDnsRouting by configurationStore.boolean(Key.ENABLE_DNS_ROUTING) { true }
- var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS)
+ var enableFakeDns by configurationStore.boolean(Key.ENABLE_FAKEDNS) { true }
var rulesProvider by configurationStore.stringToInt(Key.RULES_PROVIDER)
var logLevel by configurationStore.stringToInt(Key.LOG_LEVEL)
@@ -154,7 +159,7 @@ object DataStore : OnPreferenceDataStoreChangeListener {
var connectionTestConcurrent by configurationStore.int("connectionTestConcurrent") { 5 }
var alwaysShowAddress by configurationStore.boolean(Key.ALWAYS_SHOW_ADDRESS)
- var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.MIXED }
+ var tunImplementation by configurationStore.stringToInt(Key.TUN_IMPLEMENTATION) { TunImplementation.GVISOR }
var profileTrafficStatistics by configurationStore.boolean(Key.PROFILE_TRAFFIC_STATISTICS) { true }
var yacdURL by configurationStore.string("yacdURL") { "http://127.0.0.1:9090/ui" }
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt
index 8049cf89a..ba21dde44 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/ProfileManager.kt
@@ -200,18 +200,6 @@ object ProfileManager {
outbound = -2
)
)
- createRule(
- RuleEntity(
- name = app.getString(R.string.route_opt_block_analysis),
- domains = app.assets.open("analysis.txt").use {
- it.bufferedReader()
- .readLines()
- .filter { it.isNotBlank() }
- .joinToString("\n")
- },
- outbound = -2,
- )
- )
val fuckedCountry = mutableListOf("cn:中国")
if (Locale.getDefault().country != Locale.CHINA.country) {
// 非中文用户
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt
index 7fb409dc4..7d610c5e4 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/RuleEntity.kt
@@ -12,6 +12,8 @@ import kotlinx.parcelize.Parcelize
data class RuleEntity(
@PrimaryKey(autoGenerate = true) var id: Long = 0L,
var name: String = "",
+ @ColumnInfo(defaultValue = "")
+ var config: String = "",
var userOrder: Long = 0L,
var enabled: Boolean = false,
var domains: String = "",
@@ -31,11 +33,12 @@ data class RuleEntity(
fun mkSummary(): String {
var summary = ""
+ if (config.isNotBlank()) summary += "[config]\n"
if (domains.isNotBlank()) summary += "$domains\n"
if (ip.isNotBlank()) summary += "$ip\n"
- if (source.isNotBlank()) summary += "source: $source\n"
- if (sourcePort.isNotBlank()) summary += "sourcePort: $sourcePort\n"
- if (port.isNotBlank()) summary += "port: $port\n"
+ if (source.isNotBlank()) summary += "src ip: $source\n"
+ if (sourcePort.isNotBlank()) summary += "src port: $sourcePort\n"
+ if (port.isNotBlank()) summary += "dst port: $port\n"
if (network.isNotBlank()) summary += "network: $network\n"
if (protocol.isNotBlank()) summary += "protocol: $protocol\n"
if (packages.isNotEmpty()) summary += app.getString(
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
index cb9cade6b..ececc106e 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/SagerDatabase.kt
@@ -16,10 +16,11 @@ import kotlinx.coroutines.launch
@Database(
entities = [ProxyGroup::class, ProxyEntity::class, RuleEntity::class],
- version = 5,
+ version = 6,
autoMigrations = [
AutoMigration(from = 3, to = 4),
- AutoMigration(from = 4, to = 5)
+ AutoMigration(from = 4, to = 5),
+ AutoMigration(from = 5, to = 6)
]
)
@TypeConverters(value = [KryoConverters::class, GsonConverters::class])
@@ -33,6 +34,7 @@ abstract class SagerDatabase : RoomDatabase() {
SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs()
Room.databaseBuilder(SagerNet.application, SagerDatabase::class.java, Key.DB_PROFILE)
// .addMigrations(*SagerDatabase_Migrations.build())
+ .setJournalMode(JournalMode.TRUNCATE)
.allowMainThreadQueries()
.enableMultiInstanceInvalidation()
.fallbackToDestructiveMigration()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt b/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt
index e9296de55..d4ebf9800 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/database/preference/PublicDatabase.kt
@@ -16,6 +16,7 @@ abstract class PublicDatabase : RoomDatabase() {
val instance by lazy {
SagerNet.application.getDatabasePath(Key.DB_PROFILE).parentFile?.mkdirs()
Room.databaseBuilder(SagerNet.application, PublicDatabase::class.java, Key.DB_PUBLIC)
+ .setJournalMode(JournalMode.TRUNCATE)
.allowMainThreadQueries()
.enableMultiInstanceInvalidation()
.fallbackToDestructiveMigration()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java
index 53dda06c2..86e6fca9e 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/AbstractBean.java
@@ -144,8 +144,4 @@ public int hashCode() {
public String toString() {
return getClass().getSimpleName() + " " + JavaUtil.gson.toJson(this);
}
-
- public void applyFeatureSettings(AbstractBean other) {
- }
-
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
index 64bd0b905..8fe0a2944 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/ConfigBuilder.kt
@@ -82,7 +82,6 @@ fun buildConfig(
val globalOutbounds = HashMap()
val selectorNames = ArrayList()
val group = SagerDatabase.groupDao.getById(proxy.groupId)
- val optionsToMerge = proxy.requireBean().customConfigJson ?: ""
fun ProxyEntity.resolveChainInternal(): MutableList {
val bean = requireBean()
@@ -162,12 +161,6 @@ fun buildConfig(
external_controller = "127.0.0.1:9090"
external_ui = "../files/yacd"
}
-
- cache_file = CacheFile().apply {
- enabled = true
- store_fakeip = true
- path = "../cache/clash.db"
- }
}
log = LogOptions().apply {
@@ -261,13 +254,13 @@ fun buildConfig(
add(entity)
}
- var currentOutbound = mutableMapOf()
- lateinit var pastOutbound: MutableMap
+ var currentOutbound: SingBoxOption
+ lateinit var pastOutbound: SingBoxOption
lateinit var pastInboundTag: String
var pastEntity: ProxyEntity? = null
val externalChainMap = LinkedHashMap()
externalIndexMap.add(IndexEntity(externalChainMap))
- val chainOutbounds = ArrayList>()
+ val chainOutbounds = ArrayList()
// chainTagOut: v2ray outbound tag for this chain
var chainTagOut = ""
@@ -315,7 +308,7 @@ fun buildConfig(
outbound = tagOut
})
} else {
- pastOutbound["detour"] = tagOut
+ pastOutbound._hack_config_map["detour"] = tagOut
}
} else {
// index == 0 means last profile in chain / not chain
@@ -338,53 +331,49 @@ fun buildConfig(
type = "socks"
server = LOCALHOST
server_port = localPort
- }.asMap()
- } else { // internal outbound
+ }
+ } else {
+ // internal outbound
+
currentOutbound = when (bean) {
- is ConfigBean ->
- gson.fromJson(bean.config, currentOutbound.javaClass)
+ is ConfigBean -> CustomSingBoxOption(bean.config)
is ShadowTLSBean -> // before StandardV2RayBean
- buildSingBoxOutboundShadowTLSBean(bean).asMap()
+ buildSingBoxOutboundShadowTLSBean(bean)
is StandardV2RayBean -> // http/trojan/vmess/vless
- buildSingBoxOutboundStandardV2RayBean(bean).asMap()
+ buildSingBoxOutboundStandardV2RayBean(bean)
is HysteriaBean ->
buildSingBoxOutboundHysteriaBean(bean)
is TuicBean ->
- buildSingBoxOutboundTuicBean(bean).asMap()
+ buildSingBoxOutboundTuicBean(bean)
is SOCKSBean ->
- buildSingBoxOutboundSocksBean(bean).asMap()
+ buildSingBoxOutboundSocksBean(bean)
is ShadowsocksBean ->
- buildSingBoxOutboundShadowsocksBean(bean).asMap()
+ buildSingBoxOutboundShadowsocksBean(bean)
is WireGuardBean ->
- buildSingBoxOutboundWireguardBean(bean).asMap()
+ buildSingBoxOutboundWireguardBean(bean)
is SSHBean ->
- buildSingBoxOutboundSSHBean(bean).asMap()
+ buildSingBoxOutboundSSHBean(bean)
is AnyTLSBean ->
- buildSingBoxOutboundAnyTLSBean(bean).asMap()
+ buildSingBoxOutboundAnyTLSBean(bean)
else -> throw IllegalStateException("can't reach")
}
- currentOutbound.apply {
- // TODO nb4a keepAliveInterval?
-// val keepAliveInterval = DataStore.tcpKeepAliveInterval
-// val needKeepAliveInterval = keepAliveInterval !in intArrayOf(0, 15)
-
- if (!muxApplied) {
- val muxObj = proxyEntity.singMux()
- if (muxObj != null && muxObj.enabled) {
- muxApplied = true
- currentOutbound["multiplex"] = muxObj.asMap()
- }
+ // internal mux
+ if (!muxApplied) {
+ val muxObj = proxyEntity.singMux()
+ if (muxObj != null && muxObj.enabled) {
+ muxApplied = true
+ currentOutbound._hack_config_map["multiplex"] = muxObj.asMap()
}
}
}
@@ -394,8 +383,8 @@ fun buildConfig(
// udp over tcp
try {
val sUoT = bean.javaClass.getField("sUoT").get(bean)
- if (sUoT is Boolean && sUoT == true) {
- currentOutbound["udp_over_tcp"] = true
+ if (sUoT is Boolean && sUoT) {
+ _hack_config_map["udp_over_tcp"] = true
}
} catch (_: Exception) {
}
@@ -407,19 +396,13 @@ fun buildConfig(
domainListDNSDirectForce.add("full:$serverAddress")
}
}
- currentOutbound["domain_strategy"] =
+ _hack_config_map["domain_strategy"] =
if (forTest) "" else defaultServerDomainStrategy
- // custom JSON merge
- if (bean.customOutboundJson.isNotBlank()) {
- Util.mergeJSON(
- bean.customOutboundJson,
- currentOutbound as MutableMap
- )
- }
- }
+ _hack_config_map["tag"] = tagOut
- currentOutbound["tag"] = tagOut
+ _hack_custom_config = bean.customOutboundJson
+ }
// External proxy need a dokodemo-door inbound to forward the traffic
// For external proxy software, their traffic must goes to v2ray-core to use protected fd.
@@ -478,8 +461,8 @@ fun buildConfig(
// build outbounds
if (buildSelector) {
- val list = group?.id?.let { SagerDatabase.proxyDao.getByGroup(it) }
- list?.forEach {
+ val list = group.id.let { SagerDatabase.proxyDao.getByGroup(it) }
+ list.forEach {
tagMap[it.id] = buildChain(it.id, it)
}
outbounds.add(0, Outbound_SelectorOptions().apply {
@@ -487,7 +470,7 @@ fun buildConfig(
tag = TAG_PROXY
default_ = tagMap[proxy.id]
outbounds = tagMap.values.toList()
- }.asMap())
+ })
} else {
buildChain(0, proxy)
}
@@ -597,6 +580,8 @@ fun buildConfig(
-2L -> TAG_BLOCK
else -> if (outId == proxy.id) TAG_PROXY else tagMap[outId] ?: ""
}
+
+ _hack_custom_config = rule.config
}
if (!ruleObj.checkEmpty()) {
@@ -626,7 +611,7 @@ fun buildConfig(
for (freedom in arrayOf(TAG_DIRECT, TAG_BYPASS)) outbounds.add(Outbound().apply {
tag = freedom
type = "direct"
- }.asMap())
+ })
// Bypass Lookup for the first profile
bypassDNSBeans.forEach {
@@ -657,18 +642,17 @@ fun buildConfig(
}
}
- // remote dns obj
- remoteDns.firstOrNull().let {
- // Always use direct DNS for urlTest
- if (!forTest) dns.servers.add(DNSServerOptions().apply {
- address = it ?: throw Exception("No remote DNS, check your settings!")
- tag = "dns-remote"
- address_resolver = "dns-direct"
- strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag))
- })
- }
+ dns.servers.add(DNSServerOptions().apply {
+ address = "rcode://success"
+ tag = "dns-block"
+ })
+
+ dns.servers.add(DNSServerOptions().apply {
+ address = "local"
+ tag = "dns-local"
+ detour = TAG_DIRECT
+ })
- // add directDNS objects here
directDNS.firstOrNull().let {
dns.servers.add(DNSServerOptions().apply {
address = it ?: throw Exception("No direct DNS, check your settings!")
@@ -678,15 +662,18 @@ fun buildConfig(
strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag))
})
}
- dns.servers.add(DNSServerOptions().apply {
- address = "local"
- tag = "dns-local"
- detour = TAG_DIRECT
- })
- dns.servers.add(DNSServerOptions().apply {
- address = "rcode://success"
- tag = "dns-block"
- })
+
+ remoteDns.firstOrNull().let {
+ // Always use direct DNS for urlTest
+ if (!forTest) dns.servers.add(DNSServerOptions().apply {
+ address = it ?: throw Exception("No remote DNS, check your settings!")
+ tag = "dns-remote"
+ address_resolver = "dns-direct"
+ strategy = autoDnsDomainStrategy(SingBoxOptionsUtil.domainStrategy(tag))
+ })
+ }
+
+ dns.final_ = if (forTest) "dns-direct" else "dns-remote"
// dns object user rules
if (enableDnsRouting) {
@@ -750,16 +737,18 @@ fun buildConfig(
})
}
}
+
+ if (!forTest) _hack_custom_config = DataStore.globalCustomConfig
}.let {
+ val configMap = it.asMap()
+ Util.mergeJSON(configMap, proxy.requireBean().customConfigJson)
ConfigBuildResult(
- gson.toJson(it.asMap().apply {
- Util.mergeJSON(optionsToMerge, this)
- }),
+ gson.toJson(configMap),
externalIndexMap,
proxy.id,
trafficMap,
tagMap,
- if (buildSelector) group!!.id else -1L
+ if (buildSelector) group.id else -1L
)
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java
index 6a5f83c03..2d2e19e1d 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaBean.java
@@ -150,17 +150,6 @@ public void deserialize(ByteBufferInput input) {
}
}
- @Override
- public void applyFeatureSettings(AbstractBean other) {
- if (!(other instanceof HysteriaBean)) return;
- HysteriaBean bean = ((HysteriaBean) other);
- bean.uploadMbps = uploadMbps;
- bean.downloadMbps = downloadMbps;
- bean.allowInsecure = allowInsecure;
- bean.disableMtuDiscovery = disableMtuDiscovery;
- bean.hopInterval = hopInterval;
- }
-
@Override
public String displayAddress() {
return NetsKt.wrapIPV6Host(serverAddress) + ":" + serverPorts;
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt
index 22dfeac52..1e78a29f5 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/hysteria/HysteriaFmt.kt
@@ -272,7 +272,7 @@ fun HysteriaBean.canUseSingBox(): Boolean {
return true
}
-fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): MutableMap {
+fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): SingBoxOptions.SingBoxOption {
return when (bean.protocolVersion) {
1 -> SingBoxOptions.Outbound_HysteriaOptions().apply {
type = "hysteria"
@@ -311,7 +311,7 @@ fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): MutableMap SingBoxOptions.Outbound_Hysteria2Options().apply {
type = "hysteria2"
@@ -350,9 +350,9 @@ fun buildSingBoxOutboundHysteriaBean(bean: HysteriaBean): MutableMap mutableMapOf("error_version" to bean.protocolVersion)
+ else -> error("error_version $bean.protocolVersion")
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java
index 326662ffb..10c394f59 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksBean.java
@@ -50,13 +50,6 @@ public void deserialize(ByteBufferInput input) {
sUoT = input.readBoolean();
}
- @Override
- public void applyFeatureSettings(AbstractBean other) {
- if (!(other instanceof ShadowsocksBean)) return;
- ShadowsocksBean bean = ((ShadowsocksBean) other);
- bean.sUoT = sUoT;
- }
-
@NotNull
@Override
public ShadowsocksBean clone() {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt
index 04506abdc..b1d0bb9c1 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/shadowsocks/ShadowsocksFmt.kt
@@ -117,6 +117,10 @@ fun buildSingBoxOutboundShadowsocksBean(bean: ShadowsocksBean): SingBoxOptions.O
if (bean.plugin.isNotBlank()) {
plugin = bean.plugin.substringBefore(";")
plugin_opts = bean.plugin.substringAfter(";")
+ if (plugin == "none") {
+ plugin = null
+ plugin_opts = null
+ }
}
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java
index f02f36388..fa4aaf4e5 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/trojan_go/TrojanGoBean.java
@@ -146,15 +146,6 @@ public void deserialize(ByteBufferInput input) {
}
}
- @Override
- public void applyFeatureSettings(AbstractBean other) {
- if (!(other instanceof TrojanGoBean)) return;
- TrojanGoBean bean = ((TrojanGoBean) other);
- if (allowInsecure) {
- bean.allowInsecure = true;
- }
- }
-
@NotNull
@Override
public TrojanGoBean clone() {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt
index cad2147e6..b418a1b63 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/tuic/TuicFmt.kt
@@ -17,11 +17,21 @@ fun parseTuic(url: String): TuicBean {
protocolVersion = 5
name = link.fragment
- uuid = link.username
- token = link.password
serverAddress = link.host
serverPort = link.port
+ val rawUser = link.username
+ val rawPass = link.password
+
+ if (rawUser.contains(":")) {
+ val parts = rawUser.split(":", limit = 2)
+ uuid = parts[0]
+ token = parts.getOrElse(1) { "" }
+ } else {
+ uuid = rawUser
+ token = rawPass
+ }
+
link.queryParameter("sni")?.let {
sni = it
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java
index 61a2a7323..22adb2539 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java
+++ b/app/src/main/java/io/nekohasekai/sagernet/fmt/v2ray/StandardV2RayBean.java
@@ -112,7 +112,7 @@ public void initializeDefaultValues() {
@Override
public void serialize(ByteBufferOutput output) {
- output.writeInt(3);
+ output.writeInt(4);
super.serialize(output);
output.writeString(uuid);
output.writeString(encryption);
@@ -133,18 +133,15 @@ public void serialize(ByteBufferOutput output) {
output.writeString(earlyDataHeaderName);
break;
}
- case "http": {
+ case "http":
+ case "httpupgrade": {
output.writeString(host);
output.writeString(path);
break;
}
case "grpc": {
output.writeString(path);
- }
- case "httpupgrade": {
- output.writeString(host);
- output.writeString(path);
-
+ break;
}
}
@@ -193,17 +190,20 @@ public void deserialize(ByteBufferInput input) {
earlyDataHeaderName = input.readString();
break;
}
- case "http": {
+ case "http":
+ case "httpupgrade": {
host = input.readString();
path = input.readString();
break;
}
case "grpc": {
path = input.readString();
- }
- case "httpupgrade": {
- host = input.readString();
- path = input.readString();
+ if (version < 4) {
+ // 解决老版本数据的读取问题
+ input.readString();
+ input.readString();
+ }
+ break;
}
}
@@ -258,21 +258,6 @@ public void deserialize(ByteBufferInput input) {
}
}
- @Override
- public void applyFeatureSettings(AbstractBean other) {
- if (!(other instanceof StandardV2RayBean)) return;
- StandardV2RayBean bean = ((StandardV2RayBean) other);
- bean.allowInsecure = allowInsecure;
- bean.utlsFingerprint = utlsFingerprint;
- bean.packetEncoding = packetEncoding;
- bean.enableECH = enableECH;
- bean.echConfig = echConfig;
- bean.enableMux = enableMux;
- bean.muxPadding = muxPadding;
- bean.muxType = muxType;
- bean.muxConcurrency = muxConcurrency;
- }
-
public boolean isVLESS() {
if (this instanceof VMessBean) {
Integer aid = ((VMessBean) this).alterId;
diff --git a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
index 943674bd8..a7597999e 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/group/RawUpdater.kt
@@ -58,6 +58,7 @@ object RawUpdater : GroupUpdater() {
val response = Libcore.newHttpClient().apply {
trySocks5(DataStore.mixedPort)
+ tryH3Direct()
when (DataStore.appTLSVersion) {
"1.3" -> restrictedTLS()
}
@@ -73,6 +74,17 @@ object RawUpdater : GroupUpdater() {
subscription.subscriptionUserinfo =
Util.getStringBox(response.getHeader("Subscription-Userinfo"))
+
+ // 修改默认名字
+ if (proxyGroup.name?.startsWith("Subscription #") == true) {
+ var remoteName = Util.getStringBox(response.getHeader("content-disposition"))
+ if (remoteName.isNotBlank()) {
+ remoteName = Util.decodeFilename(remoteName)
+ if (remoteName.isNotBlank()) {
+ proxyGroup.name = remoteName
+ }
+ }
+ }
}
val proxiesMap = LinkedHashMap()
@@ -149,7 +161,9 @@ object RawUpdater : GroupUpdater() {
if (toReplace.contains(name)) {
val entity = toReplace[name]!!
val existsBean = entity.requireBean()
- existsBean.applyFeatureSettings(bean)
+ // 更新订阅,保留自定义覆写设置
+ bean.customOutboundJson = existsBean.customOutboundJson
+ bean.customConfigJson = existsBean.customConfigJson
when {
existsBean != bean -> {
changed++
@@ -463,6 +477,15 @@ object RawUpdater : GroupUpdater() {
}
}
}
+
+ "ech-opts" -> (opt.value as? Map)?.also {
+ for (echOpt in it) {
+ when (echOpt.key) {
+ "enable" -> bean.enableECH =
+ echOpt.value.toString() == "true"
+ }
+ }
+ }
}
}
proxies.add(bean)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
index 37747191e..6c0f8f45b 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Formats.kt
@@ -17,6 +17,7 @@ import io.nekohasekai.sagernet.fmt.v2ray.parseV2Ray
import moe.matsuri.nb4a.proxy.anytls.parseAnytls
import moe.matsuri.nb4a.utils.JavaUtil.gson
import moe.matsuri.nb4a.utils.Util
+import okhttp3.HttpUrl
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@@ -137,6 +138,14 @@ suspend fun parseProxies(text: String): List {
entities.add(parseHttp(this))
}.onFailure {
Logs.w(it)
+ val clashUrl = HttpUrl.Builder()
+ .scheme("https")
+ .host("install-config")
+ .addQueryParameter("url", this)
+ .build()
+ .toString()
+ .replaceFirst("https://", "clash://")
+ throw (SubscriptionFoundException(clashUrl))
}
} else if (startsWith("vmess://")) {
Logs.d("Try parse v2ray link: $this")
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt
index 98bbc20cf..3e176c8ed 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ktx/Utils.kt
@@ -20,6 +20,8 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.annotation.AttrRes
import androidx.annotation.ColorRes
import androidx.core.content.ContextCompat
+import androidx.core.view.isGone
+import androidx.core.view.isVisible
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
@@ -30,10 +32,10 @@ import com.jakewharton.processphoenix.ProcessPhoenix
import io.nekohasekai.sagernet.BuildConfig
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
-import io.nekohasekai.sagernet.bg.Executable
+import io.nekohasekai.sagernet.aidl.ISagerNetService
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.bg.SagerConnection
import io.nekohasekai.sagernet.database.DataStore
-import io.nekohasekai.sagernet.database.SagerDatabase
-import io.nekohasekai.sagernet.database.preference.PublicDatabase
import io.nekohasekai.sagernet.ui.MainActivity
import io.nekohasekai.sagernet.ui.ThemedActivity
import kotlinx.coroutines.Dispatchers
@@ -201,7 +203,7 @@ val shortAnimTime by lazy {
fun View.crossFadeFrom(other: View) {
clearAnimation()
other.clearAnimation()
- if (visibility == View.VISIBLE && other.visibility == View.GONE) return
+ if (isVisible && other.isGone) return
alpha = 0F
visibility = View.VISIBLE
animate().alpha(1F).duration = shortAnimTime
@@ -248,16 +250,33 @@ fun Fragment.needReload() {
fun Fragment.needRestart() {
snackbar(R.string.need_restart).setAction(R.string.apply) {
+ triggerFullRestart(requireContext())
+ }.show()
+}
+
+fun triggerFullRestart(ctx: Context) {
+ runOnDefaultDispatcher {
SagerNet.stopService()
- val ctx = requireContext()
- runOnDefaultDispatcher {
- delay(500)
- SagerDatabase.instance.close()
- PublicDatabase.instance.close()
- Executable.killAll(true)
+ delay(500)
+ SagerConnection.restartingApp = true
+ val connection = SagerConnection(SagerConnection.CONNECTION_ID_RESTART_BG)
+ connection.connect(ctx, RestartCallback {
ProcessPhoenix.triggerRebirth(ctx, Intent(ctx, MainActivity::class.java))
- }
- }.show()
+ })
+ }
+}
+
+private class RestartCallback(val callback: () -> Unit) : SagerConnection.Callback {
+ override fun stateChanged(
+ state: BaseService.State,
+ profileName: String?,
+ msg: String?
+ ) {
+ }
+
+ override fun onServiceConnected(service: ISagerNetService) {
+ callback()
+ }
}
fun Context.getColour(@ColorRes colorRes: Int): Int {
@@ -271,11 +290,9 @@ fun Context.getColorAttr(@AttrRes resId: Int): Int {
}
val isExpert: Boolean by lazy { BuildConfig.DEBUG || DataStore.isExpert }
-
-val isExpertFlavor = ((BuildConfig.FLAVOR == "expert") || BuildConfig.DEBUG)
const val isOss = BuildConfig.FLAVOR == "oss"
-const val isFdroid = BuildConfig.FLAVOR == "fdroid"
const val isPlay = BuildConfig.FLAVOR == "play"
+const val isPreview = BuildConfig.FLAVOR == "preview"
fun Continuation.tryResume(value: T) {
try {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt
index 54ed72a2e..f6afc4b3f 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AboutFragment.kt
@@ -10,6 +10,7 @@ import android.os.PowerManager
import android.provider.Settings
import android.text.util.Linkify
import android.view.View
+import android.widget.Toast
import androidx.activity.result.component1
import androidx.activity.result.component2
import androidx.activity.result.contract.ActivityResultContracts
@@ -28,6 +29,12 @@ import io.nekohasekai.sagernet.utils.PackageCache
import io.nekohasekai.sagernet.widget.ListListener
import libcore.Libcore
import moe.matsuri.nb4a.plugin.Plugins
+import androidx.core.net.toUri
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.database.DataStore
+import moe.matsuri.nb4a.utils.Util
+import org.json.JSONObject
class AboutFragment : ToolbarFragment(R.layout.layout_about) {
@@ -65,128 +72,133 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
}
override fun getMaterialAboutList(activityContext: Context): MaterialAboutList {
-
- var versionName = BuildConfig.VERSION_NAME
- if (!isOss) {
- versionName += " ${BuildConfig.FLAVOR}"
- }
- if (BuildConfig.DEBUG) {
- versionName += " DEBUG"
- }
-
return MaterialAboutList.Builder()
.addCard(
MaterialAboutCard.Builder()
- .outline(false)
- .addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_baseline_update_24)
- .text(R.string.app_version)
- .subText(versionName)
- .setOnClickAction {
- requireContext().launchCustomTab(
- "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases"
- )
- }
- .build())
- .addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_baseline_layers_24)
- .text(getString(R.string.version_x, "sing-box"))
- .subText(Libcore.versionBox())
- .setOnClickAction { }
- .build())
- .addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_baseline_card_giftcard_24)
- .text(R.string.donate)
- .subText(R.string.donate_info)
- .setOnClickAction {
- requireContext().launchCustomTab(
- "https://matsuridayo.github.io/index_docs/#donate"
- )
- }
- .build())
- .apply {
- PackageCache.awaitLoadSync()
- for ((_, pkg) in PackageCache.installedPluginPackages) {
- try {
- val pluginId =
- pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID)
- if (pluginId.isNullOrBlank()) continue
- addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_baseline_nfc_24)
- .text(
- getString(
- R.string.version_x,
- pluginId
- ) + " (${Plugins.displayExeProvider(pkg.packageName)})"
+ .outline(false)
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_baseline_update_24)
+ .text(R.string.app_version)
+ .subText(SagerNet.appVersionNameForDisplay)
+ .setOnClickAction {
+ requireContext().launchCustomTab(
+ "https://github.com/MatsuriDayo/NekoBoxForAndroid/releases"
+ )
+ }
+ .build())
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text(R.string.check_update_release)
+ .setOnClickAction {
+ checkUpdate(false)
+ }
+ .build())
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text(R.string.check_update_preview)
+ .setOnClickAction {
+ checkUpdate(true)
+ }
+ .build())
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_baseline_layers_24)
+ .text(getString(R.string.version_x, "sing-box"))
+ .subText(Libcore.versionBox())
+ .setOnClickAction { }
+ .build())
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_baseline_card_giftcard_24)
+ .text(R.string.donate)
+ .subText(R.string.donate_info)
+ .setOnClickAction {
+ requireContext().launchCustomTab(
+ "https://matsuridayo.github.io/index_docs/#donate"
)
- .subText("v" + pkg.versionName)
- .setOnClickAction {
- startActivity(Intent().apply {
- action =
- Settings.ACTION_APPLICATION_DETAILS_SETTINGS
- data = Uri.fromParts(
- "package", pkg.packageName, null
+ }
+ .build())
+ .apply {
+ PackageCache.awaitLoadSync()
+ for ((_, pkg) in PackageCache.installedPluginPackages) {
+ try {
+ val pluginId =
+ pkg.providers?.get(0)?.loadString(Plugins.METADATA_KEY_ID)
+ if (pluginId.isNullOrBlank()) continue
+ addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_baseline_nfc_24)
+ .text(
+ getString(
+ R.string.version_x,
+ pluginId
+ ) + " (${Plugins.displayExeProvider(pkg.packageName)})"
)
- })
- }
- .build())
- } catch (e: Exception) {
- Logs.w(e)
+ .subText("v" + pkg.versionName)
+ .setOnClickAction {
+ startActivity(Intent().apply {
+ action =
+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS
+ data = Uri.fromParts(
+ "package", pkg.packageName, null
+ )
+ })
+ }
+ .build())
+ } catch (e: Exception) {
+ Logs.w(e)
+ }
}
}
- }
- .apply {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
- val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager
- if (!pm.isIgnoringBatteryOptimizations(app.packageName)) {
- addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_baseline_running_with_errors_24)
- .text(R.string.ignore_battery_optimizations)
- .subText(R.string.ignore_battery_optimizations_sum)
- .setOnClickAction {
- requestIgnoreBatteryOptimizations.launch(
- Intent(
- Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
- Uri.parse("package:${app.packageName}")
- )
- )
- }
- .build())
+ .apply {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ val pm = app.getSystemService(Context.POWER_SERVICE) as PowerManager
+ if (!pm.isIgnoringBatteryOptimizations(app.packageName)) {
+ addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_baseline_running_with_errors_24)
+ .text(R.string.ignore_battery_optimizations)
+ .subText(R.string.ignore_battery_optimizations_sum)
+ .setOnClickAction {
+ requestIgnoreBatteryOptimizations.launch(
+ Intent(
+ Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
+ "package:${app.packageName}".toUri()
+ )
+ )
+ }
+ .build())
+ }
}
}
- }
- .build())
+ .build())
.addCard(
MaterialAboutCard.Builder()
- .outline(false)
- .title(R.string.project)
- .addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_baseline_sanitizer_24)
- .text(R.string.github)
- .setOnClickAction {
- requireContext().launchCustomTab(
- "https://github.com/MatsuriDayo/NekoBoxForAndroid"
-
- )
- }
- .build())
- .addItem(
- MaterialAboutActionItem.Builder()
- .icon(R.drawable.ic_qu_shadowsocks_foreground)
- .text(R.string.telegram)
- .setOnClickAction {
- requireContext().launchCustomTab(
- "https://t.me/MatsuriDayo"
- )
- }
+ .outline(false)
+ .title(R.string.project)
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_baseline_sanitizer_24)
+ .text(R.string.github)
+ .setOnClickAction {
+ requireContext().launchCustomTab(
+ "https://github.com/MatsuriDayo/NekoBoxForAndroid"
+
+ )
+ }
+ .build())
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .icon(R.drawable.ic_qu_shadowsocks_foreground)
+ .text(R.string.telegram)
+ .setOnClickAction {
+ requireContext().launchCustomTab(
+ "https://t.me/MatsuriDayo"
+ )
+ }
+ .build())
.build())
- .build())
.build()
}
@@ -199,6 +211,69 @@ class AboutFragment : ToolbarFragment(R.layout.layout_about) {
}
}
+ fun checkUpdate(checkPreview: Boolean) {
+ runOnIoDispatcher {
+ try {
+ val client = Libcore.newHttpClient().apply {
+ modernTLS()
+ trySocks5(DataStore.mixedPort)
+ }
+ val response = client.newRequest().apply {
+ if (checkPreview) {
+ setURL("https://api.github.com/repos/MatsuriDayo/NekoBoxForAndroid/releases/tags/preview")
+ } else {
+ setURL("https://api.github.com/repos/MatsuriDayo/NekoBoxForAndroid/releases/latest")
+ }
+ }.execute()
+ val release = JSONObject(Util.getStringBox(response.contentString))
+ val releaseName = release.getString("name")
+ val releaseUrl = release.getString("html_url")
+ var haveUpdate = releaseName.isNotBlank()
+ haveUpdate = if (isPreview) {
+ if (checkPreview) {
+ haveUpdate && releaseName != BuildConfig.PRE_VERSION_NAME
+ } else {
+ // User: 1.3.9 pre-1.4.0 Stable: 1.3.9 -> No update
+ haveUpdate && releaseName != BuildConfig.VERSION_NAME
+ }
+ } else {
+ // User: 1.4.0 Preview: pre-1.4.0 -> No update
+ // User: 1.4.0 Preview: pre-1.4.1 -> Update
+ // User: 1.4.0 Stable: 1.4.0 -> No update
+ // User: 1.4.0 Stable: 1.4.1 -> Update
+ haveUpdate && !releaseName.contains(BuildConfig.VERSION_NAME)
+ }
+ runOnMainDispatcher {
+ if (haveUpdate) {
+ val context = requireContext()
+ MaterialAlertDialogBuilder(context)
+ .setTitle(R.string.update_dialog_title)
+ .setMessage(
+ context.getString(
+ R.string.update_dialog_message,
+ SagerNet.appVersionNameForDisplay,
+ releaseName
+ )
+ )
+ .setPositiveButton(R.string.yes) { _, _ ->
+ val intent = Intent(Intent.ACTION_VIEW, releaseUrl.toUri())
+ context.startActivity(intent)
+ }
+ .setNegativeButton(R.string.no, null)
+ .show()
+ } else {
+ Toast.makeText(app, R.string.check_update_no, Toast.LENGTH_SHORT).show()
+ }
+ }
+ } catch (e: Exception) {
+ Logs.w(e)
+ runOnMainDispatcher {
+ Toast.makeText(app, e.readableMessage, Toast.LENGTH_SHORT).show()
+ }
+ }
+ }
+ }
+
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt
index 436151aae..7f3892430 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppListActivity.kt
@@ -2,7 +2,6 @@ package io.nekohasekai.sagernet.ui
import android.content.Intent
import android.content.pm.ApplicationInfo
-import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Bundle
@@ -45,6 +44,11 @@ import kotlin.coroutines.coroutineContext
class AppListActivity : ThemedActivity() {
companion object {
private const val SWITCH = "switch"
+
+ private val cachedApps
+ get() = PackageCache.installedPackages.toMutableMap().apply {
+ remove(BuildConfig.APPLICATION_ID)
+ }
}
private class ProxiedApp(
@@ -96,7 +100,8 @@ class AppListActivity : ThemedActivity() {
var filteredApps = apps
suspend fun reload() {
- apps = getCachedApps().mapNotNull { (packageName, packageInfo) ->
+ PackageCache.reload()
+ apps = cachedApps.mapNotNull { (packageName, packageInfo) ->
coroutineContext[Job]!!.ensureActive()
packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) }
}.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
@@ -156,7 +161,7 @@ class AppListActivity : ThemedActivity() {
private fun initProxiedUids(str: String = DataStore.routePackages) {
proxiedUids.clear()
- val apps = getCachedApps()
+ val apps = cachedApps
for (line in str.lineSequence()) {
val app = (apps[line] ?: continue)
val uid = app.applicationInfo?.uid ?: continue
@@ -174,14 +179,12 @@ class AppListActivity : ThemedActivity() {
val adapter = binding.list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() }
adapter.filter.filter(binding.search.text?.toString() ?: "")
- binding.list.crossFadeFrom(loading)
- }
- }
-
- fun getCachedApps(): MutableMap {
- val packages = PackageCache.installedPackages
- return packages.toMutableMap().apply {
- remove(BuildConfig.APPLICATION_ID)
+ if (apps.isEmpty()) {
+ binding.list.visibility = View.GONE
+ binding.appPlaceholder.root.crossFadeFrom(loading)
+ } else {
+ binding.list.crossFadeFrom(loading)
+ }
}
}
@@ -191,6 +194,14 @@ class AppListActivity : ThemedActivity() {
binding = LayoutAppListBinding.inflate(layoutInflater)
setContentView(binding.root)
+ binding.appPlaceholder.openSettings.setOnClickListener {
+ val intent =
+ Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = android.net.Uri.fromParts("package", packageName, null)
+ }
+ startActivity(intent)
+ }
+
setSupportActionBar(binding.toolbar)
supportActionBar?.apply {
setTitle(R.string.select_apps)
@@ -287,17 +298,6 @@ class AppListActivity : ThemedActivity() {
}
Snackbar.make(binding.list, R.string.action_import_err, Snackbar.LENGTH_LONG).show()
}
-
- R.id.uninstall_all -> {
- runOnDefaultDispatcher {
- proxiedUids.clear()
- DataStore.routePackages = ""
- apps = apps.sortedWith(compareBy({ !isProxiedApp(it) }, { it.name.toString() }))
- onMainDispatcher {
- appsAdapter.notifyItemRangeChanged(0, appsAdapter.itemCount, SWITCH)
- }
- }
- }
}
return super.onOptionsItemSelected(item)
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt
index e212cbe9c..effbc5823 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AppManagerActivity.kt
@@ -106,6 +106,7 @@ class AppManagerActivity : ThemedActivity() {
var filteredApps = apps
suspend fun reload() {
+ PackageCache.reload()
apps = cachedApps.mapNotNull { (packageName, packageInfo) ->
coroutineContext[Job]!!.ensureActive()
packageInfo.applicationInfo?.let { ProxiedApp(packageManager, it, packageName) }
@@ -184,7 +185,12 @@ class AppManagerActivity : ThemedActivity() {
val adapter = binding.list.adapter as AppsAdapter
withContext(Dispatchers.IO) { adapter.reload() }
adapter.filter.filter(binding.search.text?.toString() ?: "")
- binding.list.crossFadeFrom(loading)
+ if (apps.isEmpty()) {
+ binding.list.visibility = View.GONE
+ binding.appPlaceholder.root.crossFadeFrom(loading)
+ } else {
+ binding.list.crossFadeFrom(loading)
+ }
}
}
@@ -194,6 +200,14 @@ class AppManagerActivity : ThemedActivity() {
binding = LayoutAppsBinding.inflate(layoutInflater)
setContentView(binding.root)
+ binding.appPlaceholder.openSettings.setOnClickListener {
+ val intent =
+ Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
+ data = android.net.Uri.fromParts("package", packageName, null)
+ }
+ startActivity(intent)
+ }
+
setSupportActionBar(binding.toolbar)
supportActionBar?.apply {
setTitle(R.string.proxied_apps)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
index d63848289..25f2b6644 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/AssetsActivity.kt
@@ -211,7 +211,12 @@ class AssetsActivity : ThemedActivity() {
val localVersion = if (file.isFile) {
if (versionFile.isFile) {
- versionFile.readText().trim()
+ try {
+ versionFile.readText().trim()
+ } catch (e: Throwable) {
+ snackbar(e.readableMessage)
+ ""
+ }
} else {
"Unknown-" + DateFormat.getDateFormat(app).format(Date(file.lastModified()))
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt
index 87aa2a034..b439225f8 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/BackupFragment.kt
@@ -16,6 +16,7 @@ import com.jakewharton.processphoenix.ProcessPhoenix
import io.nekohasekai.sagernet.BuildConfig
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.bg.Executable
import io.nekohasekai.sagernet.database.*
import io.nekohasekai.sagernet.database.preference.KeyValuePair
import io.nekohasekai.sagernet.database.preference.PublicDatabase
@@ -23,6 +24,7 @@ import io.nekohasekai.sagernet.databinding.LayoutBackupBinding
import io.nekohasekai.sagernet.databinding.LayoutImportBinding
import io.nekohasekai.sagernet.databinding.LayoutProgressBinding
import io.nekohasekai.sagernet.ktx.*
+import kotlinx.coroutines.delay
import moe.matsuri.nb4a.utils.Util
import org.json.JSONArray
import org.json.JSONObject
@@ -34,33 +36,45 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) {
override fun name0() = app.getString(R.string.backup)
var content = ""
- private val exportSettings = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
- if (data != null) {
- runOnDefaultDispatcher {
- try {
- requireActivity().contentResolver.openOutputStream(
- data
- )!!.bufferedWriter().use {
- it.write(content)
- }
- onMainDispatcher {
- snackbar(getString(R.string.action_export_msg)).show()
- }
- } catch (e: Exception) {
- Logs.w(e)
- onMainDispatcher {
- snackbar(e.readableMessage).show()
+ private val exportSettings =
+ registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
+ if (data != null) {
+ runOnDefaultDispatcher {
+ try {
+ requireActivity().contentResolver.openOutputStream(
+ data
+ )!!.bufferedWriter().use {
+ it.write(content)
+ }
+ onMainDispatcher {
+ snackbar(getString(R.string.action_export_msg)).show()
+ }
+ } catch (e: Exception) {
+ Logs.w(e)
+ onMainDispatcher {
+ snackbar(e.readableMessage).show()
+ }
}
}
-
}
}
- }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = LayoutBackupBinding.bind(view)
+
+ binding.resetSettings.setOnClickListener {
+ MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.confirm)
+ .setMessage(R.string.reset_settings_message)
+ .setNegativeButton(R.string.no, null)
+ .setPositiveButton(R.string.yes) { _, _ ->
+ DataStore.configurationStore.reset()
+ triggerFullRestart(requireContext())
+ }
+ .show()
+ }
+
binding.actionExport.setOnClickListener {
runOnDefaultDispatcher {
content = doBackup(
@@ -230,9 +244,7 @@ class BackupFragment : NamedFragment(R.layout.layout_backup) {
import.backupRules.isChecked,
import.backupSettings.isChecked
)
- ProcessPhoenix.triggerRebirth(
- requireContext(), Intent(requireContext(), MainActivity::class.java)
- )
+ triggerFullRestart(requireContext())
}.onFailure {
Logs.w(it)
onMainDispatcher {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
index d91524427..054091075 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ConfigurationFragment.kt
@@ -1,8 +1,8 @@
package io.nekohasekai.sagernet.ui
+import android.annotation.SuppressLint
import android.content.Intent
import android.graphics.Color
-import android.net.Uri
import android.os.Bundle
import android.os.SystemClock
import android.provider.OpenableColumns
@@ -22,6 +22,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.appcompat.widget.SearchView
import androidx.appcompat.widget.Toolbar
+import androidx.core.net.toUri
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.view.size
@@ -92,12 +93,11 @@ import io.nekohasekai.sagernet.ui.profile.WireGuardSettingsActivity
import io.nekohasekai.sagernet.widget.QRCodeDialog
import io.nekohasekai.sagernet.widget.UndoSnackbarManager
import kotlinx.coroutines.DelicateCoroutinesApi
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
-import kotlinx.coroutines.newFixedThreadPoolContext
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import moe.matsuri.nb4a.Protocols
@@ -105,16 +105,15 @@ import moe.matsuri.nb4a.Protocols.getProtocolColor
import moe.matsuri.nb4a.proxy.anytls.AnyTLSSettingsActivity
import moe.matsuri.nb4a.proxy.config.ConfigSettingActivity
import moe.matsuri.nb4a.proxy.shadowtls.ShadowTLSSettingsActivity
+import moe.matsuri.nb4a.ui.ConnectionTestNotification
import okhttp3.internal.closeQuietly
-import java.net.InetAddress
import java.net.InetSocketAddress
import java.net.Socket
import java.net.UnknownHostException
-import java.util.Collections
+import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.zip.ZipInputStream
-import kotlin.collections.set
class ConfigurationFragment @JvmOverloads constructor(
val select: Boolean = false, val selectedItem: ProxyEntity? = null, val titleRes: Int = 0
@@ -160,6 +159,7 @@ class ConfigurationFragment @JvmOverloads constructor(
override fun onQueryTextSubmit(query: String): Boolean = false
+ @SuppressLint("DetachAndAttachSameFragment")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -317,7 +317,7 @@ class ConfigurationFragment @JvmOverloads constructor(
snackbar(getString(R.string.no_proxies_found_in_file)).show()
} else import(proxies)
} catch (e: SubscriptionFoundException) {
- (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
+ (requireActivity() as MainActivity).importSubscription(e.link.toUri())
} catch (e: Exception) {
Logs.w(e)
onMainDispatcher {
@@ -360,7 +360,7 @@ class ConfigurationFragment @JvmOverloads constructor(
snackbar(getString(R.string.no_proxies_found_in_clipboard)).show()
} else import(proxies)
} catch (e: SubscriptionFoundException) {
- (requireActivity() as MainActivity).importSubscription(Uri.parse(e.link))
+ (requireActivity() as MainActivity).importSubscription(e.link.toUri())
} catch (e: Exception) {
Logs.w(e)
@@ -597,28 +597,41 @@ class ConfigurationFragment @JvmOverloads constructor(
inner class TestDialog {
val binding = LayoutProgressListBinding.inflate(layoutInflater)
val builder = MaterialAlertDialogBuilder(requireContext()).setView(binding.root)
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- cancel()
+ .setPositiveButton(R.string.minimize) { _, _ ->
+ minimize()
}
- .setOnDismissListener {
+ .setNegativeButton(android.R.string.cancel) { _, _ ->
cancel()
}
.setCancelable(false)
lateinit var cancel: () -> Unit
- val fragment by lazy { getCurrentGroupFragment() }
- val results = Collections.synchronizedList(mutableListOf())
+ lateinit var minimize: () -> Unit
+
+ val dialogStatus = AtomicInteger(0) // 1: hidden 2: cancelled
+ var notification: ConnectionTestNotification? = null
+
+ val results: MutableSet = ConcurrentHashMap.newKeySet()
var proxyN = 0
val finishedN = AtomicInteger(0)
- suspend fun insert(profile: ProxyEntity?) {
- results.add(profile)
- }
+ fun update(profile: ProxyEntity) {
+ if (dialogStatus.get() != 2) {
+ results.add(profile)
+ }
+ runOnMainDispatcher {
+ val context = context ?: return@runOnMainDispatcher
+ val progress = finishedN.addAndGet(1)
+ val status = dialogStatus.get()
+ notification?.updateNotification(
+ progress,
+ proxyN,
+ progress >= proxyN || status == 2
+ )
+ if (status >= 1) return@runOnMainDispatcher
+ if (!isAdded) return@runOnMainDispatcher
- suspend fun update(profile: ProxyEntity) {
- fragment?.configurationListView?.post {
- val context = context ?: return@post
- if (!isAdded) return@post
+ // refresh dialog
var profileStatusText: String? = null
var profileStatusColor = 0
@@ -670,64 +683,46 @@ class ConfigurationFragment @JvmOverloads constructor(
}
binding.nowTesting.text = text
- binding.progress.text = "${finishedN.addAndGet(1)} / $proxyN"
+ binding.progress.text = "$progress / $proxyN"
}
}
}
- fun stopService() {
- if (DataStore.serviceState.started) SagerNet.stopService()
- }
-
@OptIn(DelicateCoroutinesApi::class)
@Suppress("EXPERIMENTAL_API_USAGE")
fun pingTest(icmpPing: Boolean) {
+ if (DataStore.runningTest) return else DataStore.runningTest = true
val test = TestDialog()
- val testJobs = mutableListOf()
val dialog = test.builder.show()
+ val testJobs = mutableListOf()
+ val group = DataStore.currentGroup()
+
val mainJob = runOnDefaultDispatcher {
- if (DataStore.serviceState.started) {
- stopService()
- delay(500) // wait for service stop
- }
- val group = DataStore.currentGroup()
- val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
- test.proxyN = profilesUnfiltered.size
- val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
- val testPool = newFixedThreadPoolContext(
- DataStore.connectionTestConcurrent,
- "pingTest"
- )
+ val profilesList = SagerDatabase.proxyDao.getByGroup(group.id).filter {
+ if (icmpPing) {
+ if (it.requireBean().canICMPing()) {
+ return@filter true
+ }
+ } else {
+ if (it.requireBean().canTCPing()) {
+ return@filter true
+ }
+ }
+ return@filter false
+ }
+ test.proxyN = profilesList.size
+ val profiles = ConcurrentLinkedQueue(profilesList)
repeat(DataStore.connectionTestConcurrent) {
- testJobs.add(launch(testPool) {
+ testJobs.add(launch(Dispatchers.IO) {
while (isActive) {
val profile = profiles.poll() ?: break
- if (icmpPing) {
- if (!profile.requireBean().canICMPing()) {
- profile.status = -1
- profile.error =
- app.getString(R.string.connection_test_icmp_ping_unavailable)
- test.insert(profile)
- continue
- }
- } else {
- if (!profile.requireBean().canTCPing()) {
- profile.status = -1
- profile.error =
- app.getString(R.string.connection_test_tcp_ping_unavailable)
- test.insert(profile)
- continue
- }
- }
-
profile.status = 0
- test.insert(profile)
var address = profile.requireBean().serverAddress
if (!address.isIpAddress()) {
try {
- InetAddress.getAllByName(address).apply {
+ SagerNet.underlyingNetwork!!.getAllByName(address).apply {
if (isNotEmpty()) {
address = this[0].hostAddress
}
@@ -746,7 +741,9 @@ class ConfigurationFragment @JvmOverloads constructor(
if (icmpPing) {
// removed
} else {
- val socket = Socket()
+ val socket =
+ SagerNet.underlyingNetwork?.socketFactory?.createSocket()
+ ?: Socket()
try {
socket.soTimeout = 3000
socket.bind(InetSocketAddress(0))
@@ -802,15 +799,18 @@ class ConfigurationFragment @JvmOverloads constructor(
}
testJobs.joinAll()
- testPool.close()
- onMainDispatcher {
- dialog.dismiss()
+ runOnMainDispatcher {
+ test.cancel()
}
}
test.cancel = {
+ test.dialogStatus.set(2)
+ dialog.dismiss()
runOnDefaultDispatcher {
- test.results.filterNotNull().forEach {
+ mainJob.cancel()
+ testJobs.forEach { it.cancel() }
+ test.results.forEach {
try {
ProfileManager.updateProfile(it)
} catch (e: Exception) {
@@ -818,34 +818,37 @@ class ConfigurationFragment @JvmOverloads constructor(
}
}
GroupManager.postReload(DataStore.currentGroupId())
- mainJob.cancel()
- testJobs.forEach { it.cancel() }
+ DataStore.runningTest = false
}
}
+ test.minimize = {
+ test.dialogStatus.set(1)
+ test.notification = ConnectionTestNotification(
+ dialog.context,
+ "[${group.displayName()}] ${getString(R.string.connection_test)}"
+ )
+ dialog.hide()
+ }
}
@OptIn(DelicateCoroutinesApi::class)
fun urlTest() {
+ if (DataStore.runningTest) return else DataStore.runningTest = true
val test = TestDialog()
val dialog = test.builder.show()
val testJobs = mutableListOf()
+ val group = DataStore.currentGroup()
val mainJob = runOnDefaultDispatcher {
- val group = DataStore.currentGroup()
- val profilesUnfiltered = SagerDatabase.proxyDao.getByGroup(group.id)
- test.proxyN = profilesUnfiltered.size
- val profiles = ConcurrentLinkedQueue(profilesUnfiltered)
- val testPool = newFixedThreadPoolContext(
- DataStore.connectionTestConcurrent,
- "urlTest"
- )
+ val profilesList = SagerDatabase.proxyDao.getByGroup(group.id)
+ test.proxyN = profilesList.size
+ val profiles = ConcurrentLinkedQueue(profilesList)
repeat(DataStore.connectionTestConcurrent) {
- testJobs.add(launch(testPool) {
+ testJobs.add(launch(Dispatchers.IO) {
val urlTest = UrlTest() // note: this is NOT in bg process
while (isActive) {
val profile = profiles.poll() ?: break
profile.status = 0
- test.insert(profile)
try {
val result = urlTest.doTest(profile)
@@ -866,13 +869,17 @@ class ConfigurationFragment @JvmOverloads constructor(
testJobs.joinAll()
- onMainDispatcher {
- dialog.dismiss()
+ runOnMainDispatcher {
+ test.cancel()
}
}
test.cancel = {
+ test.dialogStatus.set(2)
+ dialog.dismiss()
runOnDefaultDispatcher {
- test.results.filterNotNull().forEach {
+ mainJob.cancel()
+ testJobs.forEach { it.cancel() }
+ test.results.forEach {
try {
ProfileManager.updateProfile(it)
} catch (e: Exception) {
@@ -880,10 +887,17 @@ class ConfigurationFragment @JvmOverloads constructor(
}
}
GroupManager.postReload(DataStore.currentGroupId())
- mainJob.cancel()
- testJobs.forEach { it.cancel() }
+ DataStore.runningTest = false
}
}
+ test.minimize = {
+ test.dialogStatus.set(1)
+ test.notification = ConnectionTestNotification(
+ dialog.context,
+ "[${group.displayName()}] ${getString(R.string.connection_test)}"
+ )
+ dialog.hide()
+ }
}
inner class GroupPagerAdapter : FragmentStateAdapter(this),
@@ -1412,7 +1426,6 @@ class ConfigurationFragment @JvmOverloads constructor(
fun reloadProfiles() {
var newProfiles = SagerDatabase.proxyDao.getByGroup(proxyGroup.id)
- val subscription = proxyGroup.subscription
when (proxyGroup.order) {
GroupOrder.BY_NAME -> {
newProfiles = newProfiles.sortedBy { it.displayName() }
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt
deleted file mode 100644
index a5d33a35f..000000000
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/DebugFragment.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package io.nekohasekai.sagernet.ui
-
-import android.os.Bundle
-import android.view.View
-import io.nekohasekai.sagernet.R
-import io.nekohasekai.sagernet.database.DataStore
-import io.nekohasekai.sagernet.databinding.LayoutDebugBinding
-import io.nekohasekai.sagernet.ktx.snackbar
-
-class DebugFragment : NamedFragment(R.layout.layout_debug) {
-
- override fun name0() = "Debug"
-
- override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
- super.onViewCreated(view, savedInstanceState)
-
- val binding = LayoutDebugBinding.bind(view)
-
- binding.debugCrash.setOnClickListener {
- error("test crash")
- }
- binding.resetSettings.setOnClickListener {
- DataStore.configurationStore.reset()
- snackbar("Cleared").show()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
index ffc1f55ba..840234b54 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/GroupFragment.kt
@@ -476,12 +476,17 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
used += toLong()
}
val total = get("total=([0-9]+)")?.toLong() ?: 0
+ val remain = total - used
if (used > 0 || total > 0) {
- text += getString(
- R.string.subscription_traffic,
- used.toBytesString(),
- (total - used).toBytesString()
- )
+ text += if (remain > 0) {
+ getString(
+ R.string.subscription_traffic,
+ used.toBytesString(),
+ remain.toBytesString()
+ )
+ } else {
+ getString(R.string.subscription_used, used.toBytesString())
+ }
}
get("expire=([0-9]+)")?.apply {
text += "\n"
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt
index ef22f30b4..2f276ee9f 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/LogcatFragment.kt
@@ -9,9 +9,10 @@ import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
import android.text.style.ForegroundColorSpan
import android.view.MenuItem
import android.view.View
-import android.widget.ScrollView
+import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar
import androidx.core.view.ViewCompat
+import androidx.core.view.doOnLayout
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.databinding.LayoutLogcatBinding
import io.nekohasekai.sagernet.ktx.*
@@ -41,7 +42,6 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat),
ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener)
reloadSession()
- // TODO new logcat
}
private fun getColorForLine(line: String): ForegroundColorSpan {
@@ -75,9 +75,10 @@ class LogcatFragment : ToolbarFragment(R.layout.layout_logcat),
offset += line.length + 1
}
binding.textview.text = span
-
- binding.scroolview.post {
- binding.scroolview.fullScroll(ScrollView.FOCUS_DOWN)
+ binding.textview.clearFocus()
+ // 等 textview 完成最终 layout 再滚动到底部
+ binding.textview.doOnLayout {
+ binding.scroolview.scrollTo(0, binding.textview.height)
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
index e8a17fc50..a60012e10 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/MainActivity.kt
@@ -18,6 +18,7 @@ import androidx.preference.PreferenceDataStore
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
+import io.nekohasekai.sagernet.BuildConfig
import io.nekohasekai.sagernet.GroupType
import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
@@ -41,6 +42,7 @@ import io.nekohasekai.sagernet.group.GroupInterfaceAdapter
import io.nekohasekai.sagernet.group.GroupUpdater
import io.nekohasekai.sagernet.ktx.alert
import io.nekohasekai.sagernet.ktx.isPlay
+import io.nekohasekai.sagernet.ktx.isPreview
import io.nekohasekai.sagernet.ktx.launchCustomTab
import io.nekohasekai.sagernet.ktx.onMainDispatcher
import io.nekohasekai.sagernet.ktx.parseProxies
@@ -114,6 +116,14 @@ class MainActivity : ThemedActivity(),
)
}
}
+
+ if (isPreview) {
+ MaterialAlertDialogBuilder(this)
+ .setTitle(BuildConfig.PRE_VERSION_NAME)
+ .setMessage(R.string.preview_version_hint)
+ .setPositiveButton(android.R.string.ok, null)
+ .show()
+ }
}
fun refreshNavMenu(clashApi: Boolean) {
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt
index 6ef29c114..b8bb94145 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/NetworkFragment.kt
@@ -3,18 +3,9 @@ package io.nekohasekai.sagernet.ui
import android.content.Intent
import android.os.Bundle
import android.view.View
-import androidx.appcompat.app.AlertDialog
import io.nekohasekai.sagernet.R
-import io.nekohasekai.sagernet.database.DataStore
-import io.nekohasekai.sagernet.database.ProfileManager
import io.nekohasekai.sagernet.databinding.LayoutNetworkBinding
-import io.nekohasekai.sagernet.databinding.LayoutProgressBinding
-import io.nekohasekai.sagernet.ktx.*
-import io.nekohasekai.sagernet.utils.Cloudflare
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.runBlocking
+import io.nekohasekai.sagernet.ktx.app
class NetworkFragment : NamedFragment(R.layout.layout_network) {
@@ -27,58 +18,6 @@ class NetworkFragment : NamedFragment(R.layout.layout_network) {
binding.stunTest.setOnClickListener {
startActivity(Intent(requireContext(), StunActivity::class.java))
}
-
- //Markwon.create(requireContext())
- // .setMarkdown(binding.wrapLicense, getString(R.string.warp_license))
-
- binding.warpGenerate.setOnClickListener {
- runBlocking {
- generateWarpConfiguration()
- }
- }
- }
-
- suspend fun generateWarpConfiguration() {
- val activity = requireActivity() as MainActivity
- val binding = LayoutProgressBinding.inflate(layoutInflater).apply {
- content.setText(R.string.generating)
- }
- var job: Job? = null
- val dialog = AlertDialog.Builder(requireContext())
- .setView(binding.root)
- .setCancelable(false)
- .setNegativeButton(android.R.string.cancel) { _, _ ->
- job?.cancel()
- }
- .show()
- job = runOnDefaultDispatcher {
- try {
- val bean = Cloudflare.makeWireGuardConfiguration()
- if (isActive) {
- val groupId = DataStore.selectedGroupForImport()
- if (DataStore.selectedGroup != groupId) {
- DataStore.selectedGroup = groupId
- }
- onMainDispatcher {
- activity.displayFragmentWithId(R.id.nav_configuration)
- }
- delay(1000L)
- onMainDispatcher {
- dialog.dismiss()
- }
- ProfileManager.createProfile(groupId, bean)
- }
- } catch (e: Exception) {
- Logs.w(e)
- onMainDispatcher {
- if (isActive) {
- dialog.dismiss()
- activity.snackbar(e.readableMessage).show()
- }
- }
- }
- }
-
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/QuickDisableShortcut.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickDisableShortcut.kt
new file mode 100644
index 000000000..9571a75c8
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickDisableShortcut.kt
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv *
+ * Copyright (C) 2017 by Mygod Studio <[Email1]> *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package io.nekohasekai.sagernet.ui
+
+import android.app.Activity
+import android.content.pm.ShortcutManager
+import android.os.Build
+import android.os.Bundle
+import androidx.core.content.getSystemService
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.aidl.ISagerNetService
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.bg.SagerConnection
+
+class QuickDisableShortcut : Activity(), SagerConnection.Callback {
+ private val connection = SagerConnection(SagerConnection.CONNECTION_ID_SHORTCUT)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ connection.connect(this, this)
+ if (Build.VERSION.SDK_INT >= 25) {
+ getSystemService()!!.reportShortcutUsed("disable")
+ }
+ }
+
+ override fun onServiceConnected(service: ISagerNetService) {
+ val state = BaseService.State.values()[service.state]
+ if (state.canStop) {
+ SagerNet.stopService()
+ }
+ finish()
+ }
+
+ override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {}
+
+ override fun onDestroy() {
+ connection.disconnect(this)
+ super.onDestroy()
+ }
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/QuickEnableShortcut.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickEnableShortcut.kt
new file mode 100644
index 000000000..590868d18
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/QuickEnableShortcut.kt
@@ -0,0 +1,58 @@
+/*******************************************************************************
+ * *
+ * Copyright (C) 2017 by Max Lv <[Email0]> *
+ * Copyright (C) 2017 by Mygod Studio <[Email1]> *
+ * *
+ * This program is free software: you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation, either version 3 of the License, or *
+ * (at your option) any later version. *
+ * *
+ * This program is distributed in the hope that it will be useful, *
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of *
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
+ * GNU General Public License for more details. *
+ * *
+ * You should have received a copy of the GNU General Public License *
+ * along with this program. If not, see . *
+ * *
+ *******************************************************************************/
+
+package io.nekohasekai.sagernet.ui
+
+import android.app.Activity
+import android.content.pm.ShortcutManager
+import android.os.Build
+import android.os.Bundle
+import androidx.core.content.getSystemService
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.aidl.ISagerNetService
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.bg.SagerConnection
+
+class QuickEnableShortcut : Activity(), SagerConnection.Callback {
+ private val connection = SagerConnection(SagerConnection.CONNECTION_ID_SHORTCUT)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ connection.connect(this, this)
+ if (Build.VERSION.SDK_INT >= 25) {
+ getSystemService()!!.reportShortcutUsed("enable")
+ }
+ }
+
+ override fun onServiceConnected(service: ISagerNetService) {
+ val state = BaseService.State.values()[service.state]
+ if (state == BaseService.State.Stopped) {
+ SagerNet.startService()
+ }
+ finish()
+ }
+
+ override fun stateChanged(state: BaseService.State, profileName: String?, msg: String?) {}
+
+ override fun onDestroy() {
+ connection.disconnect(this)
+ super.onDestroy()
+ }
+}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt
index 825d24ae5..aaa062de3 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/RouteSettingsActivity.kt
@@ -39,6 +39,7 @@ import io.nekohasekai.sagernet.widget.AppListPreference
import io.nekohasekai.sagernet.widget.ListListener
import io.nekohasekai.sagernet.widget.OutboundPreference
import kotlinx.parcelize.Parcelize
+import moe.matsuri.nb4a.ui.EditConfigPreference
@Suppress("UNCHECKED_CAST")
class RouteSettingsActivity(
@@ -57,6 +58,7 @@ class RouteSettingsActivity(
fun RuleEntity.init() {
DataStore.routeName = name
+ DataStore.serverConfig = config
DataStore.routeDomain = domains
DataStore.routeIP = ip
DataStore.routePort = port
@@ -76,6 +78,7 @@ class RouteSettingsActivity(
fun RuleEntity.serialize() {
name = DataStore.routeName
+ config = DataStore.serverConfig
domains = DataStore.routeDomain
ip = DataStore.routeIP
port = DataStore.routePort
@@ -96,12 +99,10 @@ class RouteSettingsActivity(
}
}
+ private lateinit var editConfigPreference: EditConfigPreference
+
fun needSave(): Boolean {
- if (!DataStore.dirty) return false
- if (DataStore.routePackages.isBlank() && DataStore.routeDomain.isBlank() && DataStore.routeIP.isBlank() && DataStore.routePort.isBlank() && DataStore.routeSourcePort.isBlank() && DataStore.routeNetwork.isBlank() && DataStore.routeSource.isBlank() && DataStore.routeProtocol.isBlank()) {
- return false
- }
- return true
+ return DataStore.dirty
}
fun PreferenceFragmentCompat.createPreferences(
@@ -109,6 +110,16 @@ class RouteSettingsActivity(
rootKey: String?,
) {
addPreferencesFromResource(R.xml.route_preferences)
+
+ editConfigPreference = findPreference(Key.SERVER_CONFIG)!!
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ if (::editConfigPreference.isInitialized) {
+ editConfigPreference.notifyChanged()
+ }
}
val selectProfileForAdd = registerForActivityResult(
@@ -163,7 +174,7 @@ class RouteSettingsActivity(
}
}
- fun PreferenceFragmentCompat.displayPreferenceDialog(preference: Preference): Boolean {
+ fun displayPreferenceDialog(preference: Preference): Boolean {
return false
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt
index 8cda8693c..99f6f6468 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ScannerActivity.kt
@@ -4,7 +4,6 @@ import android.Manifest
import android.content.Intent
import android.content.pm.ShortcutManager
import android.graphics.ImageDecoder
-import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
@@ -13,6 +12,7 @@ import android.view.MenuItem
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.getSystemService
+import androidx.core.net.toUri
import com.google.zxing.Result
import com.king.zxing.CameraScan
import com.king.zxing.DefaultCameraScan
@@ -138,7 +138,7 @@ class ScannerActivity : ThemedActivity(),
} catch (e: SubscriptionFoundException) {
startActivity(Intent(this@ScannerActivity, MainActivity::class.java).apply {
action = Intent.ACTION_VIEW
- data = Uri.parse(e.link)
+ data = e.link.toUri()
})
} catch (e: Throwable) {
Logs.w(e)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
index 5c91718c9..18d645455 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/SettingsPreferenceFragment.kt
@@ -22,6 +22,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
private lateinit var isProxyApps: SwitchPreference
+ private lateinit var globalCustomConfig: EditConfigPreference
+
+
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -77,6 +80,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
val logLevel = findPreference(Key.LOG_LEVEL)!!
val mtu = findPreference(Key.MTU)!!
+ globalCustomConfig = findPreference(Key.GLOBAL_CUSTOM_CONFIG)!!
+ globalCustomConfig.useConfigStore(Key.GLOBAL_CUSTOM_CONFIG)
logLevel.dialogLayoutResource = R.layout.layout_loglevel_help
logLevel.setOnPreferenceChangeListener { _, _ ->
@@ -162,7 +167,7 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
resolveDestination.onPreferenceChangeListener = reloadListener
tunImplementation.onPreferenceChangeListener = reloadListener
acquireWakeLock.onPreferenceChangeListener = reloadListener
-
+ globalCustomConfig.onPreferenceChangeListener = reloadListener
}
override fun onResume() {
@@ -171,6 +176,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
if (::isProxyApps.isInitialized) {
isProxyApps.isChecked = DataStore.proxyApps
}
+ if (::globalCustomConfig.isInitialized) {
+ globalCustomConfig.notifyChanged()
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt
index bf73ca1d9..32738ea98 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/ToolsFragment.kt
@@ -7,7 +7,6 @@ import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayoutMediator
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.databinding.LayoutToolsBinding
-import io.nekohasekai.sagernet.ktx.isExpert
class ToolsFragment : ToolbarFragment(R.layout.layout_tools) {
@@ -19,8 +18,6 @@ class ToolsFragment : ToolbarFragment(R.layout.layout_tools) {
tools.add(NetworkFragment())
tools.add(BackupFragment())
- if (isExpert) tools.add(DebugFragment())
-
val binding = LayoutToolsBinding.bind(view)
binding.toolsPager.adapter = ToolsAdapter(tools)
diff --git a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt
index faec56e05..e144a26ef 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/ui/profile/ConfigEditActivity.kt
@@ -5,8 +5,12 @@ import android.content.DialogInterface
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
+import android.view.ViewGroup.MarginLayoutParams
+import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.updateLayoutParams
import androidx.core.widget.addTextChangedListener
import com.blacksquircle.ui.editorkit.insert
import com.blacksquircle.ui.language.json.JsonLanguage
@@ -17,7 +21,9 @@ import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
import io.nekohasekai.sagernet.databinding.LayoutEditConfigBinding
-import io.nekohasekai.sagernet.ktx.*
+import io.nekohasekai.sagernet.ktx.getColorAttr
+import io.nekohasekai.sagernet.ktx.readableMessage
+import io.nekohasekai.sagernet.ktx.toStringPretty
import io.nekohasekai.sagernet.ui.ThemedActivity
import io.nekohasekai.sagernet.widget.ListListener
import moe.matsuri.nb4a.ui.ExtendedKeyboard
@@ -27,6 +33,7 @@ class ConfigEditActivity : ThemedActivity() {
var dirty = false
var key = Key.SERVER_CONFIG
+ var useConfigStore = false
class UnsavedChangesDialogFragment : AlertDialogFragment() {
override fun AlertDialog.Builder.prepare(listener: DialogInterface.OnClickListener) {
@@ -49,6 +56,7 @@ class ConfigEditActivity : ThemedActivity() {
intent?.extras?.apply {
getString("key")?.let { key = it }
+ getString("useConfigStore")?.let { useConfigStore = true }
}
binding = LayoutEditConfigBinding.inflate(layoutInflater)
@@ -64,7 +72,11 @@ class ConfigEditActivity : ThemedActivity() {
binding.editor.apply {
language = JsonLanguage()
setHorizontallyScrolling(true)
- setTextContent(DataStore.profileCacheStore.getString(key)!!)
+ if (useConfigStore) {
+ setTextContent(DataStore.configurationStore.getString(key) ?: "")
+ } else {
+ setTextContent(DataStore.profileCacheStore.getString(key) ?: "")
+ }
addTextChangedListener {
if (!dirty) {
dirty = true
@@ -74,7 +86,10 @@ class ConfigEditActivity : ThemedActivity() {
}
binding.actionTab.setOnClickListener {
- binding.editor.insert(binding.editor.tab())
+ try {
+ binding.editor.insert(binding.editor.tab())
+ } catch (e: Exception) {
+ }
}
binding.actionUndo.setOnClickListener {
try {
@@ -95,11 +110,33 @@ class ConfigEditActivity : ThemedActivity() {
}
val extendedKeyboard = findViewById(R.id.extended_keyboard)
- extendedKeyboard.setKeyListener { char -> binding.editor.insert(char) }
+ extendedKeyboard.setKeyListener { char ->
+ try {
+ binding.editor.insert(char)
+ } catch (e: Exception) {
+ }
+ }
extendedKeyboard.setHasFixedSize(true)
extendedKeyboard.submitList("{},:_\"".map { it.toString() })
extendedKeyboard.setBackgroundColor(getColorAttr(R.attr.primaryOrTextPrimary))
+ val keyboardContainer = findViewById(R.id.keyboard_container)
+ ViewCompat.setOnApplyWindowInsetsListener(keyboardContainer) { v, windowInsets ->
+ val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
+ val systemBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
+ val imeVisible = windowInsets.isVisible(WindowInsetsCompat.Type.ime())
+ v.updateLayoutParams {
+ // systemBar insets are applied to the bottom of the keyboard
+ if (imeVisible) {
+ bottomMargin = imeInsets.bottom - systemBarInsets.bottom
+ } else {
+ bottomMargin = 0
+ }
+ }
+
+ WindowInsetsCompat.CONSUMED
+ }
+
ViewCompat.setOnApplyWindowInsetsListener(binding.root, ListListener)
}
@@ -119,7 +156,11 @@ class ConfigEditActivity : ThemedActivity() {
fun saveAndExit() {
formatText()?.let {
- DataStore.profileCacheStore.putString(key, it)
+ if (useConfigStore) {
+ DataStore.configurationStore.putString(key, it)
+ } else {
+ DataStore.profileCacheStore.putString(key, it)
+ }
finish()
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt
deleted file mode 100644
index b70d854a0..000000000
--- a/app/src/main/java/io/nekohasekai/sagernet/utils/Cloudflare.kt
+++ /dev/null
@@ -1,74 +0,0 @@
-package io.nekohasekai.sagernet.utils
-
-import com.wireguard.crypto.KeyPair
-import io.nekohasekai.sagernet.database.DataStore
-import io.nekohasekai.sagernet.fmt.wireguard.WireGuardBean
-import io.nekohasekai.sagernet.ktx.Logs
-import io.nekohasekai.sagernet.utils.cf.DeviceResponse
-import io.nekohasekai.sagernet.utils.cf.RegisterRequest
-import io.nekohasekai.sagernet.utils.cf.UpdateDeviceRequest
-import libcore.Libcore
-import moe.matsuri.nb4a.utils.JavaUtil.gson
-import moe.matsuri.nb4a.utils.Util
-
-// kang from wgcf
-object Cloudflare {
-
- private const val API_URL = "https://api.cloudflareclient.com"
- private const val API_VERSION = "v0a1922"
-
- private const val CLIENT_VERSION_KEY = "CF-Client-Version"
- private const val CLIENT_VERSION = "a-6.3-1922"
-
- fun makeWireGuardConfiguration(): WireGuardBean {
- val keyPair = KeyPair()
- val client = Libcore.newHttpClient().apply {
- pinnedTLS12()
- trySocks5(DataStore.mixedPort)
- }
-
- try {
- val response = client.newRequest().apply {
- setMethod("POST")
- setURL("$API_URL/$API_VERSION/reg")
- setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION)
- setHeader("Accept", "application/json")
- setHeader("Content-Type", "application/json")
- setContentString(RegisterRequest.newRequest(keyPair.publicKey))
- setUserAgent("okhttp/3.12.1")
- }.execute()
-
- Logs.d(Util.getStringBox(response.contentString))
- val device =
- gson.fromJson(Util.getStringBox(response.contentString), DeviceResponse::class.java)
- val accessToken = device.token
-
- client.newRequest().apply {
- setMethod("PATCH")
- setURL(API_URL + "/" + API_VERSION + "/reg/" + device.id + "/account/reg/" + device.id)
- setHeader("Accept", "application/json")
- setHeader("Content-Type", "application/json")
- setHeader("Authorization", "Bearer $accessToken")
- setHeader(CLIENT_VERSION_KEY, CLIENT_VERSION)
- setContentString(UpdateDeviceRequest.newRequest())
- setUserAgent("okhttp/3.12.1")
- }.execute()
-
- val peer = device.config.peers[0]
- val localAddresses = device.config.interfaceX.addresses
- return WireGuardBean().apply {
- name = "CloudFlare Warp ${device.account.id}"
- privateKey = keyPair.privateKey.toBase64()
- peerPublicKey = peer.publicKey
- serverAddress = peer.endpoint.host.substringBeforeLast(":")
- serverPort = peer.endpoint.host.substringAfterLast(":").toInt()
- localAddress = localAddresses.v4 + "/32" + "\n" + localAddresses.v6 + "/128"
- mtu = 1280
- reserved = device.config.clientId
- }
- } finally {
- client.close()
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt
index 292f929dc..2aa93fac7 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/utils/CrashHandler.kt
@@ -6,6 +6,7 @@ import android.os.Build
import android.util.Log
import com.jakewharton.processphoenix.ProcessPhoenix
import io.nekohasekai.sagernet.BuildConfig
+import io.nekohasekai.sagernet.SagerNet
import io.nekohasekai.sagernet.database.preference.PublicDatabase
import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
@@ -61,7 +62,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler {
fun buildReportHeader(): String {
var report = ""
- report += "NekoBox for Andoird ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE}) ${BuildConfig.FLAVOR.uppercase()}\n"
+ report += "NekoBox for Android ${SagerNet.appVersionNameForDisplay} (${BuildConfig.VERSION_CODE})\n"
report += "Date: ${getCurrentMilliSecondUTCTimeStamp()}\n\n"
report += "OS_VERSION: ${getSystemPropertyWithAndroidAPI("os.version")}\n"
report += "SDK_INT: ${Build.VERSION.SDK_INT}\n"
@@ -102,7 +103,7 @@ object CrashHandler : Thread.UncaughtExceptionHandler {
report += "\n"
report += pair.key + ": " + pair.toString()
}
- }catch (e: Exception) {
+ } catch (e: Exception) {
report += "Export settings failed: " + formatThrowable(e)
}
@@ -136,7 +137,8 @@ object CrashHandler : Thread.UncaughtExceptionHandler {
if (matcher.matches()) {
key = matcher.group(1)
value = matcher.group(2)
- if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] = value
+ if (key != null && value != null && !key.isEmpty() && !value.isEmpty()) systemProperties[key] =
+ value
}
}
bufferedReader.close()
diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt
index a2e09ef58..c5e9370c7 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/utils/PackageCache.kt
@@ -67,15 +67,6 @@ object PackageCache {
operator fun get(uid: Int) = uidMap[uid]
operator fun get(packageName: String) = packageMap[packageName]
- suspend fun awaitLoad() {
- if (::packageMap.isInitialized) {
- return
- }
- loaded.withLock {
- // just await
- }
- }
-
fun awaitLoadSync() {
if (::packageMap.isInitialized) {
return
diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt
deleted file mode 100644
index 874304dc2..000000000
--- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/DeviceResponse.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-package io.nekohasekai.sagernet.utils.cf
-
-
-import com.google.gson.annotations.SerializedName
-
-data class DeviceResponse(
- @SerializedName("created")
- var created: String = "",
- @SerializedName("type")
- var type: String = "",
- @SerializedName("locale")
- var locale: String = "",
- @SerializedName("enabled")
- var enabled: Boolean = false,
- @SerializedName("token")
- var token: String = "",
- @SerializedName("waitlist_enabled")
- var waitlistEnabled: Boolean = false,
- @SerializedName("install_id")
- var installId: String = "",
- @SerializedName("warp_enabled")
- var warpEnabled: Boolean = false,
- @SerializedName("name")
- var name: String = "",
- @SerializedName("fcm_token")
- var fcmToken: String = "",
- @SerializedName("tos")
- var tos: String = "",
- @SerializedName("model")
- var model: String = "",
- @SerializedName("id")
- var id: String = "",
- @SerializedName("place")
- var place: Int = 0,
- @SerializedName("config")
- var config: Config = Config(),
- @SerializedName("updated")
- var updated: String = "",
- @SerializedName("key")
- var key: String = "",
- @SerializedName("account")
- var account: Account = Account()
-) {
- data class Config(
- @SerializedName("peers")
- var peers: List = listOf(),
- @SerializedName("services")
- var services: Services = Services(),
- @SerializedName("interface")
- var interfaceX: Interface = Interface(),
- @SerializedName("client_id")
- var clientId: String = ""
- ) {
- data class Peer(
- @SerializedName("public_key")
- var publicKey: String = "",
- @SerializedName("endpoint")
- var endpoint: Endpoint = Endpoint()
- ) {
- data class Endpoint(
- @SerializedName("v6")
- var v6: String = "",
- @SerializedName("host")
- var host: String = "",
- @SerializedName("v4")
- var v4: String = ""
- )
- }
-
- data class Services(
- @SerializedName("http_proxy")
- var httpProxy: String = ""
- )
-
- data class Interface(
- @SerializedName("addresses")
- var addresses: Addresses = Addresses()
- ) {
- data class Addresses(
- @SerializedName("v6")
- var v6: String = "",
- @SerializedName("v4")
- var v4: String = ""
- )
- }
- }
-
- data class Account(
- @SerializedName("account_type")
- var accountType: String = "",
- @SerializedName("role")
- var role: String = "",
- @SerializedName("referral_renewal_countdown")
- var referralRenewalCountdown: Int = 0,
- @SerializedName("created")
- var created: String = "",
- @SerializedName("usage")
- var usage: Int = 0,
- @SerializedName("warp_plus")
- var warpPlus: Boolean = false,
- @SerializedName("referral_count")
- var referralCount: Int = 0,
- @SerializedName("license")
- var license: String = "",
- @SerializedName("quota")
- var quota: Int = 0,
- @SerializedName("premium_data")
- var premiumData: Int = 0,
- @SerializedName("id")
- var id: String = "",
- @SerializedName("updated")
- var updated: String = ""
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt
deleted file mode 100644
index 34bbe7c16..000000000
--- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/RegisterRequest.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.nekohasekai.sagernet.utils.cf
-
-import com.google.gson.Gson
-import com.google.gson.annotations.SerializedName
-import com.wireguard.crypto.Key
-import java.text.SimpleDateFormat
-import java.util.*
-
-data class RegisterRequest(
- @SerializedName("fcm_token") var fcmToken: String = "",
- @SerializedName("install_id") var installedId: String = "",
- var key: String = "",
- var locale: String = "",
- var model: String = "",
- var tos: String = "",
- var type: String = ""
-) {
-
- companion object {
- fun newRequest(publicKey: Key): String {
- val request = RegisterRequest()
- request.fcmToken = ""
- request.installedId = ""
- request.key = publicKey.toBase64()
- request.locale = "en_US"
- request.model = "PC"
- val format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'000000'+08:00", Locale.US)
- request.tos = format.format(Date())
- request.type = "Android"
- return Gson().toJson(request)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt b/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt
deleted file mode 100644
index e12915b41..000000000
--- a/app/src/main/java/io/nekohasekai/sagernet/utils/cf/UpdateDeviceRequest.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.nekohasekai.sagernet.utils.cf
-
-import com.google.gson.Gson
-
-data class UpdateDeviceRequest(
- var name: String, var active: Boolean
-) {
- companion object {
- fun newRequest(name: String = "SagerNet Client", active: Boolean = true) =
- Gson().toJson(UpdateDeviceRequest(name, active))
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
index 82572f574..05f89275f 100644
--- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
+++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptions.java
@@ -1,19 +1,97 @@
package moe.matsuri.nb4a;
-import static moe.matsuri.nb4a.utils.JavaUtil.gson;
-
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import com.google.gson.ToNumberPolicy;
+import com.google.gson.TypeAdapter;
+import com.google.gson.TypeAdapterFactory;
import com.google.gson.annotations.SerializedName;
+import com.google.gson.reflect.TypeToken;
+import java.lang.reflect.Type;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import moe.matsuri.nb4a.utils.Util;
+
public class SingBoxOptions {
// base
+ private static final Gson gsonSingbox = new GsonBuilder()
+ .registerTypeHierarchyAdapter(SingBoxOption.class, new SingBoxOptionSerializer())
+ .setPrettyPrinting()
+ .setNumberToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+ .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
+ .setLenient()
+ .disableHtmlEscaping()
+ .create();
+
public static class SingBoxOption {
+
+ public transient Map _hack_config_map; // 仍然用普通json方式合并,所以Object内不要使用 _hack
+
+ public transient String _hack_custom_config;
+
+ public SingBoxOption() {
+ _hack_config_map = new HashMap<>();
+ }
+
public Map asMap() {
- return gson.fromJson(gson.toJson(this), Map.class);
+ return gsonSingbox.fromJson(gsonSingbox.toJson(this), Map.class);
+ }
+
+ }
+
+ public static final class CustomSingBoxOption extends SingBoxOption {
+
+ public transient String config;
+
+ public CustomSingBoxOption(String config) {
+ super();
+ this.config = config;
+ }
+
+ public Map getBasicMap() {
+ Map map = gsonSingbox.fromJson(config, Map.class);
+ if (map == null) {
+ map = new HashMap<>();
+ }
+ return map;
+ }
+ }
+
+ // 自定义序列化器
+ public static class SingBoxOptionSerializer implements JsonSerializer {
+ @Override
+ public JsonElement serialize(SingBoxOption src, Type typeOfSrc, JsonSerializationContext context) {
+ // 拿到原始的 delegate(默认序列化器)
+ TypeAdapter> delegate = gsonSingbox.getDelegateAdapter(
+ new TypeAdapterFactory() {
+ @Override
+ public TypeAdapter create(Gson gson, TypeToken type) {
+ return null; // 返回 null,表示只作为“跳过当前自定义”的 marker
+ }
+ },
+ TypeToken.get(src.getClass())
+ );
+ Map map;
+ if (src instanceof CustomSingBoxOption) {
+ map = ((CustomSingBoxOption) src).getBasicMap();
+ } else {
+ map = gsonSingbox.fromJson(((TypeAdapter) delegate).toJson(src), Map.class);
+ }
+ if (src._hack_config_map != null && !src._hack_config_map.isEmpty()) {
+ Util.INSTANCE.mergeMap(map, src._hack_config_map);
+ }
+ if (src._hack_custom_config != null && !src._hack_custom_config.isBlank()) {
+ Util.INSTANCE.mergeJSON(map, src._hack_custom_config);
+ }
+ return gsonSingbox.toJsonTree(map);
}
}
@@ -33,7 +111,7 @@ public static class MyOptions extends SingBoxOption {
public List inbounds;
- public List> outbounds;
+ public List outbounds;
public RouteOptions route;
diff --git a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt
index 19f733bdd..d3971b2b2 100644
--- a/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt
+++ b/app/src/main/java/moe/matsuri/nb4a/SingBoxOptionsUtil.kt
@@ -157,5 +157,7 @@ fun SingBoxOptions.Rule_DefaultOptions.checkEmpty(): Boolean {
if (port?.isNotEmpty() == true) return false
if (port_range?.isNotEmpty() == true) return false
if (source_ip_cidr?.isNotEmpty() == true) return false
+ //
+ if (!_hack_custom_config.isNullOrBlank()) return false
return true
}
diff --git a/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt b/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt
index 27c08e310..9b125aeea 100644
--- a/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt
+++ b/app/src/main/java/moe/matsuri/nb4a/net/LocalResolverImpl.kt
@@ -6,16 +6,14 @@ import android.os.CancellationSignal
import android.system.ErrnoException
import androidx.annotation.RequiresApi
import io.nekohasekai.sagernet.SagerNet
-import io.nekohasekai.sagernet.ktx.tryResumeWithException
+import io.nekohasekai.sagernet.ktx.Logs
+import io.nekohasekai.sagernet.ktx.runOnIoDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
-import kotlinx.coroutines.runBlocking
import libcore.ExchangeContext
import libcore.LocalDNSTransport
import java.net.InetAddress
import java.net.UnknownHostException
-import kotlin.coroutines.resume
-import kotlin.coroutines.suspendCoroutine
object LocalResolverImpl : LocalDNSTransport {
@@ -27,110 +25,126 @@ object LocalResolverImpl : LocalDNSTransport {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
+ override fun networkHandle(): Long {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ return SagerNet.underlyingNetwork?.networkHandle ?: 0
+ }
+ return 0
+ }
+
@RequiresApi(Build.VERSION_CODES.Q)
override fun exchange(ctx: ExchangeContext, message: ByteArray) {
- return runBlocking {
- suspendCoroutine { continuation ->
- val signal = CancellationSignal()
- ctx.onCancel(signal::cancel)
- val callback = object : DnsResolver.Callback {
- override fun onAnswer(answer: ByteArray, rcode: Int) {
- // exchange don't generate rcode error
- ctx.rawSuccess(answer)
- continuation.resume(Unit)
- }
+ val signal = CancellationSignal()
+ ctx.onCancel(signal::cancel)
- override fun onError(error: DnsResolver.DnsException) {
- when (val cause = error.cause) {
- is ErrnoException -> {
- ctx.errnoCode(cause.errno)
- continuation.resume(Unit)
- return
- }
- }
- continuation.tryResumeWithException(error)
- }
+ val callback = object : DnsResolver.Callback {
+ override fun onAnswer(answer: ByteArray, rcode: Int) {
+ ctx.rawSuccess(answer)
+ }
+
+ override fun onError(error: DnsResolver.DnsException) {
+ val cause = error.cause
+ if (cause is ErrnoException) {
+ ctx.errnoCode(cause.errno)
+ } else {
+ Logs.w(error)
+ ctx.errnoCode(114514)
}
- DnsResolver.getInstance().rawQuery(
- SagerNet.underlyingNetwork,
- message,
- DnsResolver.FLAG_NO_RETRY,
- Dispatchers.IO.asExecutor(),
- signal,
- callback
- )
}
}
+
+ DnsResolver.getInstance().rawQuery(
+ SagerNet.underlyingNetwork,
+ message,
+ DnsResolver.FLAG_NO_RETRY,
+ Dispatchers.IO.asExecutor(),
+ signal,
+ callback
+ )
}
override fun lookup(ctx: ExchangeContext, network: String, domain: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
- return runBlocking {
- suspendCoroutine { continuation ->
- val signal = CancellationSignal()
- ctx.onCancel(signal::cancel)
- val callback = object : DnsResolver.Callback> {
- override fun onAnswer(answer: Collection, rcode: Int) {
- if (rcode == 0) {
- ctx.success((answer as Collection).mapNotNull { it?.hostAddress }
- .joinToString("\n"))
- } else {
- ctx.errorCode(rcode)
- }
- continuation.resume(Unit)
- }
+ val signal = CancellationSignal()
+ ctx.onCancel(signal::cancel)
- override fun onError(error: DnsResolver.DnsException) {
- when (val cause = error.cause) {
- is ErrnoException -> {
- ctx.errnoCode(cause.errno)
- continuation.resume(Unit)
- return
- }
- }
- continuation.tryResumeWithException(error)
+ val callback = object : DnsResolver.Callback> {
+ override fun onAnswer(answer: Collection, rcode: Int) {
+ try {
+ if (rcode == 0) {
+ ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
+ } else {
+ ctx.errorCode(rcode)
}
+ } catch (e: Exception) {
+ Logs.w(e)
+ ctx.errnoCode(114514)
}
- val type = when {
- network.endsWith("4") -> DnsResolver.TYPE_A
- network.endsWith("6") -> DnsResolver.TYPE_AAAA
- else -> null
- }
- if (type != null) {
- DnsResolver.getInstance().query(
- SagerNet.underlyingNetwork,
- domain,
- type,
- DnsResolver.FLAG_NO_RETRY,
- Dispatchers.IO.asExecutor(),
- signal,
- callback
- )
- } else {
- DnsResolver.getInstance().query(
- SagerNet.underlyingNetwork,
- domain,
- DnsResolver.FLAG_NO_RETRY,
- Dispatchers.IO.asExecutor(),
- signal,
- callback
- )
+ }
+
+ override fun onError(error: DnsResolver.DnsException) {
+ try {
+ val cause = error.cause
+ if (cause is ErrnoException) {
+ ctx.errnoCode(cause.errno)
+ } else {
+ Logs.w(error)
+ ctx.errnoCode(114514)
+ }
+ } catch (e: Exception) {
+ Logs.w(e)
+ ctx.errnoCode(114514)
}
}
}
+
+ val type = when {
+ network.endsWith("4") -> DnsResolver.TYPE_A
+ network.endsWith("6") -> DnsResolver.TYPE_AAAA
+ else -> null
+ }
+ if (type != null) {
+ DnsResolver.getInstance().query(
+ SagerNet.underlyingNetwork,
+ domain,
+ type,
+ DnsResolver.FLAG_NO_RETRY,
+ Dispatchers.IO.asExecutor(),
+ signal,
+ callback
+ )
+ } else {
+ DnsResolver.getInstance().query(
+ SagerNet.underlyingNetwork,
+ domain,
+ DnsResolver.FLAG_NO_RETRY,
+ Dispatchers.IO.asExecutor(),
+ signal,
+ callback
+ )
+ }
} else {
- val answer = try {
- val u = SagerNet.underlyingNetwork
- if (u != null) {
- u.getAllByName(domain)
- } else {
- InetAddress.getAllByName(domain)
+ runOnIoDispatcher {
+ // 老版本系统,继续用阻塞的 InetAddress
+ try {
+ val u = SagerNet.underlyingNetwork
+ val answer = try {
+ u?.getAllByName(domain)
+ } catch (e: UnknownHostException) {
+ null
+ } ?: InetAddress.getAllByName(domain)
+ if (answer != null) {
+ ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
+ } else {
+ ctx.errnoCode(114514)
+ }
+ } catch (e: UnknownHostException) {
+ ctx.errorCode(RCODE_NXDOMAIN)
+ } catch (e: Exception) {
+ Logs.w(e)
+ ctx.errnoCode(114514)
}
- } catch (e: UnknownHostException) {
- ctx.errorCode(RCODE_NXDOMAIN)
- return
}
- ctx.success(answer.mapNotNull { it.hostAddress }.joinToString("\n"))
}
}
diff --git a/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java b/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java
index b3f18a0f2..df436de76 100644
--- a/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java
+++ b/app/src/main/java/moe/matsuri/nb4a/proxy/config/ConfigBean.java
@@ -4,6 +4,7 @@
import com.esotericsoftware.kryo.io.ByteBufferInput;
import com.esotericsoftware.kryo.io.ByteBufferOutput;
+import com.google.gson.JsonObject;
import org.jetbrains.annotations.NotNull;
@@ -49,7 +50,16 @@ public String displayName() {
}
public String displayType() {
- return type == 0 ? "sing-box config" : "sing-box outbound";
+ if (type != null && type == 1 && JavaUtil.isNotBlank(config)) {
+ try {
+ JsonObject json = JavaUtil.gson.fromJson(config, JsonObject.class);
+ if (json != null && json.has("type")) {
+ return json.get("type").getAsString() + " (sing-box)";
+ }
+ } catch (Exception ignored) {
+ }
+ }
+ return type != null && type == 0 ? "sing-box config" : "sing-box outbound";
}
@NotNull
diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt
index 69a3edb73..da87e9c24 100644
--- a/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt
+++ b/app/src/main/java/moe/matsuri/nb4a/ui/ColorPickerPreference.kt
@@ -20,7 +20,6 @@ import androidx.preference.PreferenceViewHolder
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.ktx.getColorAttr
-import io.nekohasekai.sagernet.ktx.isExpertFlavor
import kotlin.math.roundToInt
class ColorPickerPreference
diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt
new file mode 100644
index 000000000..31bc7a193
--- /dev/null
+++ b/app/src/main/java/moe/matsuri/nb4a/ui/ConnectionTestNotification.kt
@@ -0,0 +1,29 @@
+package moe.matsuri.nb4a.ui
+
+import android.content.Context
+import androidx.core.app.NotificationCompat
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.ktx.Logs
+
+class ConnectionTestNotification(val context: Context, val title: String) {
+ private val channelId = "connection-test"
+ private val notificationId = 1001
+
+ fun updateNotification(progress: Int, max: Int, finished: Boolean) {
+ try {
+ if (finished) {
+ SagerNet.notification.cancel(notificationId)
+ return
+ }
+ val builder = NotificationCompat.Builder(context, channelId)
+ .setSmallIcon(R.drawable.ic_service_active)
+ .setContentTitle(title)
+ .setOnlyAlertOnce(true)
+ .setContentText("$progress / $max").setProgress(max, progress, false)
+ SagerNet.notification.notify(notificationId, builder.build())
+ } catch (e: Exception) {
+ Logs.w(e)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt
index 6b2ea819a..1292ed5c5 100644
--- a/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt
+++ b/app/src/main/java/moe/matsuri/nb4a/ui/EditConfigPreference.kt
@@ -4,8 +4,10 @@ import android.content.Context
import android.content.Intent
import android.util.AttributeSet
import androidx.preference.Preference
+import io.nekohasekai.sagernet.Key
import io.nekohasekai.sagernet.R
import io.nekohasekai.sagernet.database.DataStore
+import io.nekohasekai.sagernet.ktx.Logs
import io.nekohasekai.sagernet.ktx.app
import io.nekohasekai.sagernet.ui.profile.ConfigEditActivity
@@ -26,9 +28,27 @@ class EditConfigPreference : Preference {
intent = Intent(context, ConfigEditActivity::class.java)
}
+ var configKey = Key.SERVER_CONFIG
+ var useConfigStore = false
+
+ fun useConfigStore(key: String) {
+ try {
+ this.configKey = key
+ useConfigStore = true
+ intent = intent!!.apply {
+ putExtra("useConfigStore", "1")
+ putExtra("key", key)
+ }
+ } catch (e: Exception) {
+ Logs.w(e)
+ }
+ }
+
override fun getSummary(): CharSequence {
- val config = DataStore.serverConfig
- return if (DataStore.serverConfig.isBlank()) {
+ val config =
+ (if (useConfigStore) DataStore.configurationStore.getString(configKey) else DataStore.serverConfig)
+ ?: ""
+ return if (config.isBlank()) {
return app.resources.getString(androidx.preference.R.string.not_set)
} else {
app.resources.getString(R.string.lines, config.split('\n').size)
diff --git a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt
index 35df4ee61..c5ae09abb 100644
--- a/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt
+++ b/app/src/main/java/moe/matsuri/nb4a/utils/Util.kt
@@ -5,6 +5,8 @@ import android.content.Context
import android.util.Base64
import libcore.StringBox
import java.io.ByteArrayOutputStream
+import java.net.URLDecoder
+import java.nio.charset.StandardCharsets
import java.text.SimpleDateFormat
import java.util.*
import java.util.zip.Deflater
@@ -132,12 +134,12 @@ object Util {
} else if (v is List<*>) {
if (k.startsWith("+")) { // prepend
val dstKey = k.removePrefix("+")
- var currentList = (dst[dstKey] as List<*>).toMutableList()
+ var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf()
currentList = (v + currentList).toMutableList()
dst[dstKey] = currentList
} else if (k.endsWith("+")) { // append
val dstKey = k.removeSuffix("+")
- var currentList = (dst[dstKey] as List<*>).toMutableList()
+ var currentList = (dst[dstKey] as? List<*>)?.toMutableList() ?: mutableListOf()
currentList = (currentList + v).toMutableList()
dst[dstKey] = currentList
} else {
@@ -150,7 +152,7 @@ object Util {
return dst
}
- fun mergeJSON(j: String, dst: MutableMap) {
+ fun mergeJSON(dst: MutableMap, j: String) {
if (j.isBlank()) return
val src = JavaUtil.gson.fromJson(j, dst.javaClass)
mergeMap(dst, src)
@@ -188,4 +190,11 @@ object Util {
}
return ""
}
+
+ fun decodeFilename(headerValue: String): String {
+ val regex = Regex("filename\\*=[^']*''(.+)")
+ val match = regex.find(headerValue)
+ val encoded = match?.groupValues?.get(1) ?: ""
+ return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name())
+ }
}
diff --git a/app/src/main/res/layout/layout_app_list.xml b/app/src/main/res/layout/layout_app_list.xml
index 45432fa45..4128a887f 100644
--- a/app/src/main/res/layout/layout_app_list.xml
+++ b/app/src/main/res/layout/layout_app_list.xml
@@ -116,4 +116,8 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/layout_apps_item" />
+
+
diff --git a/app/src/main/res/layout/layout_app_placeholder.xml b/app/src/main/res/layout/layout_app_placeholder.xml
new file mode 100644
index 000000000..456ac8a2b
--- /dev/null
+++ b/app/src/main/res/layout/layout_app_placeholder.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_apps.xml b/app/src/main/res/layout/layout_apps.xml
index 1dd32b265..e648248a2 100644
--- a/app/src/main/res/layout/layout_apps.xml
+++ b/app/src/main/res/layout/layout_apps.xml
@@ -153,4 +153,8 @@
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/layout_apps_item" />
+
+
diff --git a/app/src/main/res/layout/layout_backup.xml b/app/src/main/res/layout/layout_backup.xml
index fb61aaf98..81a7decee 100644
--- a/app/src/main/res/layout/layout_backup.xml
+++ b/app/src/main/res/layout/layout_backup.xml
@@ -5,6 +5,14 @@
android:orientation="vertical"
android:padding="16dp">
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/src/main/res/menu/app_list_neko_menu.xml b/app/src/main/res/menu/app_list_neko_menu.xml
deleted file mode 100644
index 94cbd0782..000000000
--- a/app/src/main/res/menu/app_list_neko_menu.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 0d7e634ea..b756ddc32 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -23,6 +23,8 @@
أسم المجموعة
الجلاد
تبديل
+ تمكين
+ تعطيل
غير مجمعة
وثيقة
موضوع
diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml
index 56567ae01..a140392cc 100644
--- a/app/src/main/res/values-be/strings.xml
+++ b/app/src/main/res/values-be/strings.xml
@@ -100,6 +100,8 @@
Трафік
Перамыкач
Пераключыць
+ Уключыць
+ Выключыць
Разгрупаваны
Дакумент
Тэма
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index baa382951..4bd07aeb5 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -5,6 +5,8 @@
Gruppen name
Umschalter
Umschalten
+ Aktivieren
+ Deaktivieren
Ungruppiert
Dokument
Style
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index e0301bcf7..fda3913b3 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -14,7 +14,9 @@
Tema
Documento
Desagrupado
- Alternar
+ Alternar
+ Habilitar
+ Deshabilitar
Conmutador
Tráfico
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index bb9291cb2..5e61ffe05 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -17,6 +17,8 @@
مستندات
گروه پیشفرض
تغییر وضعیت
+ فعالسازی
+ غیرفعالسازی
سوییچر
ترافیک
sing-box تنظیمات
@@ -208,7 +210,6 @@
Trojan Go
Mieru
Naïve
- Ping Tunnel
Hysteria
SSH
WireGuard
@@ -521,10 +522,7 @@
فعال کردن ECH
فعال کردن ECH
تنظیمات ECH
- فعال کردن پشتیبانی از امضای مجوز post-quantum همتا
- اندازه تطبیقی رکوردهای TLS را غیرفعال میکند
پیکربندی ECH
- اگر فعال باشد، همیشه از بزرگترین اندازه رکورد TLS ممکن استفاده میشود. در صورت غیرفعال کردن، اندازه رکوردهای TLS ممکن است در تلاش برای بهبود تاخیر تنظیم شود.
میزبان HTTPUpgrade
مسیر HTTPUpgrade
بهروزرسانی اشتراک این گروه
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 86f4728be..8a69b076d 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -57,6 +57,8 @@
Mettre à jour
Nom du groupe
Basculer
+ Activer
+ Désactiver
Dégroupé
Document
Thème
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index b52bb0c11..d09ed3ca7 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -83,6 +83,8 @@
Nama grup
Pengalih
Beralih
+ Aktifkan
+ Nonaktifkan
Tidak bergrup
Dokumen
Tema
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index bcd7649b9..10f7b36c1 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -35,6 +35,8 @@
グループ名
スイッチャー
トグル
+ 有効化
+ 無効化
グループ化されていない
ドキュメント(英語)
テーマ
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 742d825ff..edc95c6d6 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -9,6 +9,8 @@
프로젝트
코틀린에 기록 된 안드로이드에 대한 보편적 인 프록시 도구 체인.
비녀장
+ 활성화
+ 비활성화
그룹 해제
문서
테마
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index f9472e690..8b07363b1 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -319,6 +319,8 @@
Oppdater
Bytter
Veksle
+ Aktiver
+ Deaktiver
Konfigurasjon
Den universelle proxy -verktøykjeden for Android, skrevet i Kotlin.
Raffinering
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 07a38d946..bc8c4b2ae 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1,5 +1,12 @@
+Введите запрос
+Удалить запрос
+Поиск
+Отправить запрос
+Голосовой поиск
+Поделиться с помощью
+Свернуть
Использовать WakeLock
Сохранять CPU включенным
СКАЧАТЬ
@@ -28,6 +35,8 @@
Всегда показывать адрес
Всегда отображать адрес сервера на карте конфигурации
Универсальный набор инструментов прокси для Android, написанный на Kotlin.
+NekoBox
+NekoBox для Android
Минимальная версия TLS протокола
Версия
Добавить HTTP-прокси к VPN
@@ -38,6 +47,8 @@
Авто
Автоматическое подключение
Включить прокси при запуске/обновлении приложения, если он был запущен до этого
+Автоматический выбор прокси-приложений
+Автоматический выбор прокси-приложений приведет к очистке текущего выбора.
Автоматическое обновление
Задержка автоматического обновления (в минутах)
Бэкап
@@ -64,12 +75,15 @@
Настройки цепочки
Введено символов: %1$d из %2$d
Превышено ограничение на количество символов (%1$d из %2$d)
+Обновления не найдены.
+Проверить наличие обновлений предварительной версии
+Проверить наличие обновлений релизнойной версии
Циклическая ссылка
Маршрут не может содержать сам себя.
-Очистить Logcat
+Очистить журнал
Очистить
Вы уверены, что хотите очистить эту группу?
-Очистить выбор
+Очистить выбранное
Очистить текстовое поле
Очистить статистику трафика
Передача HTTP-трафика в виде открытого текста небезопасна
@@ -79,9 +93,8 @@
Подтвердить
Подключиться
Подключение…
-Тест подключения
-Успех: соединение HTTP заняло %d мс
-Успех: соединение HTTP заняло %d мс
+HTTPS-соединение установлено за %d мс
+HTTP-соединение установлено за %d мс
Очистить результаты теста
Удалить недоступные
Домен не найден
@@ -139,6 +152,7 @@
Включить мультиплексор
Метод шифрования
Шифрование
+Ошибка: недопустимое значение
Ошибка
Ошибка
Дополнительно
@@ -155,20 +169,20 @@
Создание…
Исходный код
Всегда разрешать небезопасные
-"\"Создано:
-%s\""
+Добавлено:
+%s
Базовый
-"\"Обновлено:
-%s\""
+Обновлено:
+%s
Создать группу
Создать подписку
Разгруппировано
Вы уверены, что хотите удалить эту группу?
-"\"Удалено:
-%s\""
+Удалено:
+%s
различия (%s)
-"\"Дубликат:
-%s\""
+Дубликат:
+%s
Редактировать группу
Редактировать подписку
Фильтр
@@ -185,15 +199,18 @@
Пустая группа
Еще не обновлено
%d прокси
-%d Прокси | Обновлено %s
+%d прокси | Обновлено %s
Ссылка для подписки
Тип группы
Обновить
-%s: Обновлено %d прокси
+%s: обновлено %d прокси
Имя службы gRPC
Тип заголовка
+Интервал переключения портов(второй)
HTTP-хост
HTTP-путь
+HTTP-хост обновления
+HTTP-путь обновления
Полезные данные аутентификации
Тип аутентификации
Окно получения соединения QUIC
@@ -211,8 +228,9 @@
Установить из Play Store
Недействительный файл резервной копии
Неверное имя сервера
-Инвертировать выбор
+Инвертировать выбранное
Маршрут IPv6
+Сделать JSON исходящим
Вкладка
Ключ (необязательно)
Обратный прокси
@@ -221,11 +239,13 @@
Тип журнала
Удерживайте для настройки размера буфера.
Экспорт отладочной информации
+Закрыть
Выберите AM (до полудня) или PM (после полудня)
AM
PM
О приложении
Конфигурация
+Панель sing-box
Группы
Журналы
Маршруты
@@ -233,34 +253,42 @@
Трафик
Подсказка о соединении с лимитным тарифным планом
Подсказывать системе, что VPN следует рассматривать как сеть с лимитным тарифным планом
+Свернуть
Отсутствующий плагин
Переместить
Новое уведомление
-Удалить \"%1$s\"
+Флажок установлен.
+Флажки установлены частично.
+Флажок не установлен.
+Удалить "%1$s"
Новых уведомлений больше %1$d
Перейти к следующему месяцу
Перейти к предыдущему месяцу
Отмена
ОК
Столбец со днями недели: %1$s
+Перейти к текущему году: %1$d
Перейти к %1$s году
Сохранить
Перейти в режим выбора дней
Нажмите, чтобы перейти к выбору дня
Перейти в режим ввода текста
Нажмите, чтобы перейти к выбору года
+Удерживайте для настройки своего MTU.
Число одновременных подключений Mux
Мультиплекс
Протокол мультиплексирования предназначен для уменьшения задержки соединения TCP, а не для увеличения пропускной способности. Использование мультиплексирования для просмотра видео, загрузки или проверки скорости обычно неэффективно. Если сервер не поддерживает мультиплексирование, вы не сможете получить доступ к Интернету.
Протокол мультиплексирования
Небезопасный параллелизм
-"\"Использовать N параллельных соединений для повышения отказоустойчивости в условиях ненадёжной сети. Большее число соединений увеличивает риск обнаружения туннелей и снижает безопасность. Этот проект стремится к максимальной защите от анализа трафика, следовательно, использование его небезопасным образом противоречит данной цели.
+Использовать N параллельных соединений для повышения отказоустойчивости в условиях ненадёжной сети. Большее число соединений увеличивает риск обнаружения туннелей и снижает безопасность. Этот проект стремится к максимальной защите от анализа трафика, следовательно, использование его небезопасным образом противоречит данной цели.
-Если придётся использовать данную функцию, попробуйте N=2, чтобы проверить, решит ли это проблему. Настоятельно не рекомендуется использовать более 4 соединений.\""
+Если придётся использовать данную функцию, попробуйте N=2, чтобы проверить, решит ли это проблему. Настоятельно не рекомендуется использовать более 4 соединений.
+Провести проверку NAT
+STUN сервер
Перезагрузите прокси-сервис, чтобы применить изменения
Перезапустите приложение для применения изменений
Сеть
-Сбрасывать исходящие соединения при изменении сети
+Сбросить исходящие соединения при изменении сети
Ночной режим
Нет
В ссылке не найдено прокси-серверов
@@ -286,7 +314,7 @@
Этот плагин может не работать с автоподключением
Настроить…
Отключено
-Профиль %s требует плагин %s, но поставщик вашего проприетарного оборудования (обычно это большие компании, занимающиеся слежкой и производители вредоносного ПО) изменил ваш Android, сделав плагин непригодным для использования.
+Профиль %s требует плагин %s, но поставщик вашего проприетарного оборудования (обычно это большие компании, занимающиеся слежкой и производители вредоносного ПО) изменил ваш Android, сделав плагин непригодным для использования.
Неизвестный плагин %s
Предупреждение: похоже, что этот плагин не из известного надежного источника.
Порт прокси HTTP
@@ -294,28 +322,35 @@
Порт прокси SOCKS5
Транспрокси-порт
Предпочитать
-Текст \"%1$s\" скопирован в буфер обмена
+Текст "%1$s" скопирован в буфер обмена
+Это приложение является предварительной версией и может содержать множество проблем. Если вы не хотите тестировать его, скачайте релизную версию с GitHub!
Конфигурация профиля
Пожалуйста, выберите профиль
Импортировать профиль
Подтвердите, что хотите импортировать профиль %s?
Имя профиля
-Профиль %s требует установки подключаемого модуля %s, но он не найден.
+Профиль %s требует установки подключаемого модуля %s, но он не найден.
Статистика трафика профиля
Когда отключено, использованный трафик не будет отображаться
Проект
Протокол
Параметры протокола
Настройки протокола
+Версия протокола
Режим VPN для приложений
Настроить режим VPN для выбранных приложений
Настройки сервера
Прокси-цепочка
Переключить
+Включить
+Выключить
Не удалось запустить службу VPN. Возможно, вам потребуется перезагрузить устройство.
+Отключить WakeLock
Удаленный DNS
Удалить дубликаты серверов
Сбросить соединения
+Восстановить настройки по умолчанию
+Восстановить настройки по умолчанию, такие данные, как узлы и группы, будут сохранены. Для полной очистки данных, очистите данные приложения непосредственно в системных настройках.
Определить адрес назначения
Если адрес назначения является доменом, то он передается в соответствии с правилом IPv6.
Создать маршрут
@@ -334,6 +369,7 @@
Не файл ресурса: ожидался .db, а не %s
Блокировать рекламу
Блокировать аналитику
+Блокировать QUIC
Обход LAN
Правило Play Store для %s
Выбрать профиль…
@@ -345,6 +381,8 @@
Сканирование…
Поиск…
Поиск
+Очистить текстовое поле
+Назад
Шифрование транспортного уровня
Настройки безопасности
Выберите приложения
@@ -357,21 +395,25 @@
Прокси-сервис
Служба обновления подписки
VPN-сервис
+Установить URL панель
Настройки
Simple Obfs (плагин Shadowsocks для Android)
V2Ray (плагин Shadowsocks для Android)
Поделиться
QR-код
+Поделиться подпиской
Показать нижнюю панель, как в SagerNet
Показать прямую скорость
Также в уведомлении показывать скорость трафика без прокси
Показывать название группы в уведомлении
Показать системные приложения
-Server Name Indication (SNI)
+Боковая панель
+SNI (индикация имени сервера)
+Результат проверки для адрес назначения
Перехватывать результат для Маршрутизации
%s/с
-"Прокси :%1$s↑ %2$s↓
-Прямой :%3$s↑%4$s↓"
+Прокси :%1$s↑ %2$s↓
+\nПрямой :%3$s↑%4$s↓
Интервал обновления уведомлений о скорости
Настройки Shadowsocks
Без аутентификации
@@ -380,41 +422,48 @@
Открытый ключ
Стандарт
Старт
->999
+>999
Остановить
Выключение…
Пожалуйста, подождите…
Обнаружение поведения NAT
Определить поведение сопоставления адресов NAT и поведение фильтрации NAT в соответствии с RFC 5780 при помощи STUN.
Подписка
+Истекает: %s
Импортировать подписку
Подтвердите, что хотите импортировать подписку %s? Если вы импортируете из ненадежного источника, это может привести к утечке вашего IP-адреса и такого поведения.
Настройки подписки
-%s Использовано / %s Осталось
+%s Использовано / %s Осталось
Тип подписки
Обновление подписки
Обновление %s …
%s Использовано
UserAgent
Подписки
-%1$s, %2$s
%d TCP-соединений
Интервал доставки пакетов TCP keep-alive
Канал обновлений в Telegram
+Число параллельных подключений
Тема
Переключатель
Использовать TLS
Настройки защиты TLS
Сеть
-%1$s↑ %2$s↓
Включить анализ трафика
Провайдер Trojan
+Контроллер перегрузки
+Отключить SNI
+Включить 0-RTT QUIC-соединение
+Режим UDP-ретранслятора
Реализация TUN
%d UDP-соединений
Недоступен
Отменить
Есть несохраненные изменения. Сохранить?
+Обновить все подписки
Обновить подписку текущей группы
+Текущая версия: %1$s\nДоступная версия: %2$s\nХотите загрузить её?
+Доступна новая версия
Обновить настройки
Прокси-сервер не подключен, вы уверены, что хотите продолжить обновление?
Обновлять только при подключении
@@ -428,17 +477,17 @@
ВКЛ.
Версия (%s)
Подключено, нажмите для проверки подключения
-%s
Отказано в разрешении на создание службы VPN
-Сбрасывать исходящие соединения, при выходе из спящего режима
+Сбросить исходящие соединения при выходе из спящего режима
Создать конфигурацию
-CloudFlare Warp - это бесплатный VPN-провайдер Wire Guard. Используя его, вы соглашаетесь с условиями использования.
+CloudFlare Warp - это бесплатный VPN-провайдер WireGuard. Используя его, вы соглашаетесь с условиями использования.
Частный адрес
Пароль сервера
Открытый ключ сервера
Пересылка через браузер
Перенаправьте соответствующие веб-сокеты через браузер.
Хост WebSocket
+Максимальный объём данных
Путь к WebSocket
Flow (подпротокол VLESS)
Да
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 2afcf4b51..891a0a128 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -71,6 +71,8 @@
Değiştirici
Grup adı
Aç/Kapat
+ Etkinleştir
+ Devre Dışı Bırak
Gruplandırılmamış
Döküman
Tema
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 2134a358b..400d69374 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -1,130 +1,525 @@
- Локальний порт DNS
- Переписування домену
- Може призвести до перезапуску інших програм для повторного підключення до мережі після зупинки проксі
- Увімкніть FakeDNS
- Вирішуйте домени в обхідних маршрутах за допомогою Direct DNS. Будьте в курсі можливих витоків DNS
- Увімкнути маршрутизацію DNS
- Прямий DNS
- Віддалений DNS
- Налаштування DNS
- Код помилки: #%d
- Інтернет недоступний
- Не вдалося: %s
- Успішно: рукостискання HTTP зайняло %dms
- Успіх: рукостискання HTTPS зайняло %dms
- Тестування…
- Проксі: %1$s↑ %2$s↓
-\nПрямий: %3$s↑ %4$s↓
- Вимкнути перевірку сертифікатів. Якщо ввімкнено, ця конфігурація є такою ж безпечною, як і відкритий текст
- Дозволити незахищене
- Налаштування безпеки
- У сповіщенні також покажіть швидкість трафіку без проксі -сервера
- Показати пряму швидкість
- Якщо ви не хочете використовувати Quick Tile як перемикач
- Показати кнопку зупинки
- Різні налаштування
- Інтервал оновлення сповіщення про швидкість
- Пересилайте відповідні WebSockets через браузер.
- Переадресація браузера
- Налаштування WebSocket
- Увімкнути вхідний протокол HTTP
- Налаштування програми
- Вхідні налаштування
- Прив’язати вхідні сервери до 0.0.0.0
- Дозволити підключення з локальної мережі
- Налаштування маршруту
- Транспроксі -порт
- Проксі -порт HTTP
- Порт проксі -сервера SOCKS5
- Тільки проксі
- Режим обслуговування
- Дублікат:
-\n%s
- Видалено:
-\n%s
- Оновлено:
-\n%s
- Додано:
-\n%s
- Ви впевнені, що хочете видалити цю групу\?
- Різниця (%s)
- Різниця
- %s: Оновлено %d проксі
- %s: Різниці немає
- %d проксі | Оновлено на %s
- %d проксі
- Ще не оновлено
- Порожня група
- Редагувати підписку
- Редагувати групу
- Створити підписку
- Створити групу
- Потрібна назва групи
- Посилання на підписку
- Оновлення
- Назва групи
- Перемикач
- Переключити
- Розгрупований
- Документ
- Тема
- Про
- Група
- Конфігурація
- Експорт інформації про налагодження
- Версія (%s)
- Версія
- Ліцензії з відкритим кодом
- Канал оновлення Telegram
- Вихідний код
- Проєкт
+ NekoBox
+ NekoBox for Android
Універсальний набір інструментів проксі для Android, написаний на Kotlin.
- Назва маршруту
- Ви впевнені, що хочете видалити цей маршрут\?
- Створити маршрут
- Маршрут
- Система підказок щодо розгляду VPN як лічильника
- Підказка з дозуванням
- Тільки
- Краще
- Маршрут IPv6
- Редагувати конфігурацію
- Тип конфігурації
- Додаткові заголовки
- Шифрування
- Назва раннього заголовка даних
- Сертифікати
- Переговори про протокол шару додатків
- Індикація імені сервера
- Використовуйте TLS
- ім\'я служби gRPC
- Шлях HTTP
- Хост HTTP
- Шлях WebSocket
- Хост WebSocket
- Тип заголовка
- Мережа
- Шифрування транспортного рівня
- Змінити ідентифікатор
- ідентифікатор користувача
- Протокол Param
- Протокол
- Метод шифрування
- Пароль
- Ключ (необов’язково)
- Пароль (необов’язково)
- Ім\'я користувача (необов’язково)
- Ім\'я користувача
- Віддалений порт
- Сервер
- Імя профілю
- URL -адреса перевірки з\'єднання
- Режим Transproxy
+ Проєкт
+ Вихідний код
+ Канал оновлень в Telegram
+ Ліцензії з відкритим кодом
+ Версія
+ Версія (%s)
+ Експорт інформації про налагодження
+ Конфігурація
+ Група
+ Про додаток
+ Тема
+ Документація
+ Без групи
+ Перемикач
+ Увімкнути
+ Вимкнути
+ Перемикач
+ Трафік
+ Панель керування sing-box
+
+ Копіювати
+ Відкрити
+
+ Назва групи
+ Оновити
+ Посилання на підписку
+ Потрібна назва групи
+ Створити групу
+ Створити підписку
+ Оновити всі підписки
+ Редагувати групу
+ Редагувати підписку
+ Порожня група
+ Ще не оновлено
+ %d проксі
+ %d проксі | Оновлено %s
+ %s: Немає різниці
+ %s: Оновлено %d проксі
+ Різниця
+ Різниця (%s)
+ Ви впевнені, що хочете видалити цю групу?
+ Додано: \n%s
+ Оновлено: \n%s
+ Видалено: \n%s
+ Дублікат: \n%s
+
+ Режим роботи
+ Тільки проксі
+ VPN
+ Порт проксі-сервера SOCKS5
+ Проксі-порт HTTP
+ Транспроксі-порт
+ Налаштування маршруту
+ Дозволити підключення з локальної мережі
+ Прив’язати вхідні сервери до 0.0.0.0
+ Вхідні налаштування
+ Налаштування програми
+ Увімкнути вхідний протокол HTTP
+ Налаштування WebSocket
+ Max early data
+ Переадресація браузера
+ Пересилайте відповідні WebSockets через браузер.
+ Інтервал оновлення сповіщення про швидкість
+ Різні налаштування
+ Показати кнопку зупинки
+ Якщо ви не хочете використовувати Quick Tile як перемикач
+ Показати пряму швидкість
+ У сповіщенні також покажіть швидкість трафіку без проксі-сервера
+ Налаштування безпеки TLS
+ Дозволити незахищене
+ Вимкнути перевірку сертифікатів. Якщо ввімкнено, ця конфігурація є такою ж безпечною, як і відкритий текст
+ %1$s↑ %2$s↓
+ Проксі: %1$s↑ %2$s↓\nПрямий: %3$s↑ %4$s↓
+ %s/с
+ Тестування…
+ Успіх: рукостискання HTTPS зайняло %dмс
+ Успіх: рукостискання HTTP зайняло %dмс
+ Не вдалося: %s
+ Інтернет недоступний
+ Код помилки: #%d
+ Налаштування DNS
+ Віддалений DNS
+ Прямий DNS
+ Увімкнути маршрутизацію DNS
+ Вирішуйте домени в обхідних маршрутах за допомогою Direct DNS. Будьте в курсі можливих витоків DNS
+ Увімкнути FakeDNS
+ Може призвести до перезапуску інших програм для повторного підключення до мережі після зупинки проксі
+ Переписування домену
+ Локальний порт DNS
Увімкнути вхідний сигнал Transproxy
- Obfs Парам
+ Режим Transproxy
+ URL-адреса перевірки з\'єднання
+
+ Ім\'я профілю
+ Сервер
+ Віддалений порт
+ Ім\'я користувача
+ Ім\'я користувача (необов’язково)
+ Пароль (необов’язково)
+ Ключ (необов’язково)
+ Пароль
+ Метод шифрування
+ Протокол
+ Параметр протоколу
Obfs
- %s/s
- трафіку
-
\ No newline at end of file
+ Параметр Obfs
+ ідентифікатор користувача
+ Змінити ідентифікатор
+ Шифрування транспортного рівня
+ Мережа
+ Тип заголовка
+ Хост WebSocket
+ Шлях WebSocket
+ Хост HTTP
+ Шлях HTTP
+ ім\'я служби gRPC
+ Використовувати TLS
+ Індикація імені сервера
+ Переговори про протокол шару додатків
+ Сертифікати
+ Назва раннього заголовка даних
+ Шифрування
+ Додаткові заголовки
+ Тип конфігурації
+ Редагувати конфігурацію
+
+ Маршрут IPv6
+ Віддавати перевагу
+ Тільки
+ Лімітне підключення
+ Повідомляти системі, що VPN-з\'єднання є лімітним
+ Маршрут
+ Створити маршрут
+ Ви впевнені, що хочете видалити цей маршрут?
+ Назва маршруту
+ Проксі
+ В обхід
+ Блокувати
+ Вибрати профіль…
+ Порожній маршрут
+ Встановіть правила перед збереженням
+ Правило домену для %s
+ Правило Play Store для %s
+ Правило IP для %s
+ Скинути
+ Керування ресурсами маршрутів
+ Ресурси
+ Постачальник ресурсів правил
+ Офіційний
+ Локальна версія: %s
+ Немає оновлень
+ Оновлено
+ Це не файл ресурсів: очікувався .db, але отримано %s
+ Обходити локальну мережу
+ Блокувати рекламу
+ Блокувати аналітику
+ Блокувати QUIC
+ Стратегія визначення доменів
+ Увімкнути аналіз трафіку
+ Увімкнути мультиплексор
+ Mux призначений для зменшення затримки рукостискання TCP, а не для збільшення пропускної здатності з\'єднання. Використання Mux для перегляду відео, завантаження або тестування швидкості зазвичай є контрпродуктивним. Якщо сервер його не підтримує, ви не зможете отримати доступ до Інтернету.
+ Паралельні з\'єднання Mux
+ Інтервал доставки пакетів TCP keep-alive
+ Програми в режимі VPN
+ Налаштувати режим VPN для вибраних програм
+ Увімк
+ Вимк
+ Пошук…
+ Сканування…
+ Інвертувати вибір
+ Очистити вибір
+ В обхід
+ Показувати системні програми
+ Автоматичне підключення
+ Вмикати проксі під час запуску/оновлення програми, якщо він працював раніше
+ Дозволити перемикання на екрані блокування
+ Інформація про ваш вибраний профіль буде менш захищеною
+
+ Служба VPN
+ Служба проксі
+ Проксі запущено.
+ Неправильне ім\'я сервера
+ Не вдалося:
+ Зупинити
+ Зупинка…
+ %s
+ Дозвіл на створення служби VPN відхилено
+ Не вдалося запустити службу VPN. Можливо, вам доведеться перезавантажити пристрій.
+
+ Будь ласка, виберіть профіль
+ Підключити
+ Буфер обміну порожній
+
+ Налаштування
+ Редагувати
+ Поділитися
+ Додати профіль
+ Вибрати профіль
+ SOCKS
+ HTTP
+ Shadowsocks
+ ShadowsocksR
+ VMess
+ Trojan
+ Trojan Go
+ Mieru
+ Naïve
+ AnyTLS
+ Hysteria
+ SSH
+ WireGuard
+ TUIC
+ Ланцюжок проксі
+ Власна конфігурація
+ Балансувальник
+ Налаштування балансувальника
+ Тип
+ Стратегія
+ Налаштування ланцюжка
+ Налаштування конфігурації
+ Циклічне посилання
+ Маршрут не може містити самого себе.
+ Очистити
+ Порожня група
+ З підписки
+ Сканувати китайські програми
+ Експортувати у файл
+ Експортувати в буфер обміну
+ Експорт
+ Імпортувати з буфера обміну
+ Імпортувати з файлу
+ Експортовано успішно!
+ Не вдалося експортувати.
+ Імпортовано успішно!
+ Не вдалося імпортувати.
+ На вашому пристрої відсутній стандартний файловий менеджер Android, будь ласка, встановіть його, наприклад, Material Files.
+
+
+ Конфігурація профілю
+ Видалити
+ Ви впевнені, що хочете видалити цей профіль?
+ QR-код
+ Сканувати QR-код
+ Ручні налаштування
+
+ - Видалено
+ - Видалено %d елементи
+ - Видалено %d елементів
+ - Видалено %d елемента
+
+
+ - Додано
+ - Додано %d проксі
+ - Додано %d проксі
+ - Додано %d проксі
+
+ Скасувати
+ Незахищений
+ Застарілий
+
+
+ Підключення…
+ Підключено, торкніться, щоб перевірити з\'єднання
+ Не підключено
+
+ Підписки
+ HTTP-трафік у відкритому вигляді є незахищеним
+ Помилка
+ Проксі за посиланням не знайдено
+ У підписці не знайдено проксі
+ У файлі не знайдено проксі
+ У буфері обміну не знайдено проксі
+ Дедуплікація
+ Пожертвувати
+ Я люблю гроші
+ Ігнорувати оптимізацію батареї
+ Зняти деякі обмеження
+
+ Плагін
+ Налаштувати…
+ Вимкнено
+ Невідомий плагін %s
+ Попередження: цей плагін, здається, не з надійного джерела.
+ Цей плагін може не працювати з автоматичним підключенням
+ Налаштування сервера
+ Налаштування Shadowsocks
+ Зміни не збережено. Ви хочете зберегти?
+ Так
+ Ні
+ Застосувати
+ Перезавантажте службу проксі, щоб застосувати зміни
+ Ліцензія
+ Переконайтеся, що ви прочитали документацію перед додаванням власних правил, інакше ви можете не підключитися до Інтернету.
+ %d рядків
+ Нічний режим
+ Як у системі
+ Увімкнути
+ Вимкнути
+ Авто
+ Увімкнути журнал
+ Для налагодження
+ Список
+ Випадковий
+ Ping
+ Порт API
+ Інтервал спостереження балансувальника
+ Стандартний
+ V2RayN
+ %dмс
+ Недоступний
+ Завжди показувати адресу
+ Завжди відображати адресу сервера на картці конфігурації
+ Очистити статистику трафіку
+ Тест з\'єднання
+ Очистити результати тесту
+ TCPing
+ TCPing недоступний
+ ICMPing
+ ICMPing недоступний
+ URL Test
+ Домен не знайдено
+ З\'єднання відхилено
+ Недоступний
+ Час очікування минув
+ Додати HTTP-проксі до VPN
+ HTTP-проксі буде використовуватися безпосередньо (браузером / деякими підтримуваними програмами), не проходячи через віртуальний мережевий пристрій (Android 10+)
+ Налаштування протоколу
+ Постачальник Trojan
+ Основні
+ Налаштування групи
+ Підписка
+ Налаштування підписки
+ Тип групи
+ Тип підписки
+ Ви впевнені, що хочете видалити цю групу?
+ Примусове визначення
+ Визначати всі доменні імена в IP-адреси під час оновлення. Хост і SNI будуть автоматично додані, якщо це можливо
+ Видаляти дублікати конфігурацій під час оновлення
+ Raw
+ Налаштування оновлення
+ Автоматичне оновлення
+ Затримка автооновлення (у хвилинах)
+ Оновлювати лише при підключенні
+ Запобігати витоку IP-адреси
+ UserAgent
+ Підтвердити
+ Відсутній плагін
+ Профіль %s вимагає встановлення плагіна %s, але його не знайдено.
+ ДІЗНАТИСЯ БІЛЬШЕ
+ ЗАВАНТАЖИТИ
+ Встановити з Play Store
+ Встановити з F-Droid
+ Завантажити
+ OOCv1 API Token
+ Неправильний токен OOCv1
+ Проксі не підключено, ви впевнені, що хочете продовжити оновлення?
+ Попередження
+ Підписка вимагає підтримки протоколу %s, але його не знайдено. Непідтримувані профілі будуть проігноровані.
+ Служба оновлення підписок
+ Оновлення підписки
+ Оновлення %s …
+ Фільтр
+ Використано %s
+ Використано %s / Залишилося %s
+ Закінчується: %s
+ Імпортувати підписку
+ Підтвердьте, що ви хочете імпортувати підписку %s? Якщо ви робите це з ненадійного джерела, це може призвести до витоку вашої IP-адреси та даних про цю дію.
+ Імпортувати профіль
+ Підтвердьте, що ви хочете імпортувати профіль %s?
+ Ви впевнені, що хочете очистити цю групу?
+ Програми
+ Вибрати програми
+ %d програм
+ Пароль обфускації
+ Тип автентифікації
+ Дані автентифікації
+ Макс. швидкість вивантаження (у Мбіт/с)
+ Макс. швидкість завантаження (у Мбіт/с)
+ Експериментальні
+ Вікно прийому потоку QUIC
+ Вікно прийому з\'єднання QUIC
+ Вимкнути виявлення Path MTU
+ Порядок
+ За замовчуванням
+ За назвою
+ За затримкою
+ Профіль %s вимагає плагін %s, але ваш виробник обладнання (зазвичай гіганти-капіталісти зі спостереження та виробники шкідливого ПЗ) змінив вашу систему Android, зробивши плагін непридатним для використання.
+ Simple Obfs (плагін для Shadowsocks)
+ V2Ray (плагін для Shadowsocks)
+ %s | %s/с ↑
+ %s | %s/с ↓
+ Всього %s ↑
+ Всього %s ↓
+ %d TCP-з\'єднань
+ %d UDP-з\'єднань
+ Копіювати назву
+ Копіювати назву пакета
+ Відкрити програму
+ Відкрити налаштування
+ Відкрити в маркеті
+ Створити правило
+ Скопійовано успішно!
+ Не вдалося скопіювати.
+ Програма не має інтерфейсу.
+ Правило для %s
+ Правило маршрутизації %s залежить від VPN, щоб набути чинності, тому воно ігнорується.
+ Статистика трафіку профілю
+ Якщо вимкнено, використаний трафік не буде враховуватися
+ Статистики ще немає
+ Статистика трафіку програм вимкнена
+ Немає
+ Публічний ключ
+ Приватний ключ
+ Парольна фраза приватного ключа
+ Платформа для перекладу
+ Інструменти
+ Журнали
+ Очистити Logcat
+ Локальна адреса
+ Публічний ключ піра
+ Pre-Shared Key піра
+ Cloudflare Warp
+ CloudFlare Warp – це безкоштовний VPN-провайдер, що використовує WireGuard. Використовуючи його, ви погоджуєтеся з Умовами надання послуг.
+ Згенерувати конфігурацію
+ Генерація…
+ Реалізація TUN
+ Перевизначити призначення
+ Використовувати перехоплений домен для перезапису адреси призначення, а не лише для маршрутизації
+ Визначати призначення
+ Якщо адреса призначення є доменом, вона передається далі на основі стратегії IPv6 (конфліктує з FakeDNS)
+ Pcap
+ Файли Pcap будуть збережені в %s
+ Небезпечна паралельність
+ Використовуйте N паралельних тунельних з\'єднань для більшої стійкості в поганих мережевих умовах. Більша кількість з\'єднань полегшує виявлення тунелювання та робить його менш безпечним. Цей проєкт прагне до найсильнішого захисту від аналізу трафіку. Використання його небезпечним способом суперечить його меті. \n\nЯкщо ви все ж таки повинні це використовувати, спробуйте спочатку N=2, щоб побачити, чи це вирішить ваші проблеми. Наполегливо не рекомендується використовувати більше 4 з\'єднань.
+ Виявлення поведінки NAT
+ Визначити поведінку відображення NAT клієнта та поведінку фільтрації NAT, визначену в RFC 3478, за допомогою STUN.
+ Почати
+ Це може зайняти кілька хвилин…
+ STUN-сервер
+ Результат перевірки NAT
+ Мережа
+ MTU
+ Резервне копіювання
+ Групи та конфігурації
+ Правила маршрутизації
+ Налаштування
+ Якщо налаштування маршрутизації не будуть збережені разом з конфігураціями, власні вихідні з\'єднання будуть втрачені.
+ Не є файлом резервної копії: очікувався .json, але отримано %s
+ Неправильний файл резервної копії
+ Імпорт
+ Імпортування перезапише наявні дані.
+ Імпортування…
+ Кодування пакетів
+ Захоплювати WakeLock
+ Відпускати WakeLock
+ Не давати процесору заснути
+ Перемкнути
+ Режим ретрансляції UDP
+ Контролер перевантаження
+ Вимкнути SNI
+ Увімкнути 0-RTT QUIC рукостискання
+ Ваша програма занадто стара (%s) і перестане працювати %s. Будь ласка, оновіть її!
+ Ваша програма занадто стара (%s) і перестала працювати %s. Будь ласка, оновіть її!
+ Очистити недоступні
+ Перемістити
+ Пріоритетний постачальник плагіна
+ Створити ярлик
+ Мінімальна версія TLS для підписки
+ Інтервал зміни портів (секунди)
+ Стратегія доменів для віддаленого
+ Стратегія доменів для прямого
+ Стратегія доменів для адреси сервера
+ Показувати нижню панель, як у SagerNet
+ Відбиток uTLS
+ Власний JSON вихідного з\'єднання
+ Власний JSON конфігурації
+ Встановлений JSON є лише вихідним з\'єднанням
+ Зберегти
+ Встановити URL панелі
+ Увімкнути Clash API
+ Рівень журналу
+ Надати Clash API та панель yacd за адресою 127.0.0.1:9090
+ Flow (підпротокол VLESS)
+ Реклама
+ Обходити ЛВС в ядрі
+ Перезапустіть програму, щоб застосувати зміни
+ Використовувати селектор
+ Передній проксі
+ Проксі-призначення
+ ShadowTLS
+ Версія протоколу
+ Поділитися підпискою
+ Показувати назву групи у сповіщенні
+ Скинути з\'єднання
+ Видалити дублікати серверів
+ Довге натискання на налаштування, щоб встановити власний MTU.
+ Довге натискання на налаштування, щоб встановити розмір буфера.
+ Налаштування маскування TLS
+ Паралелізм тесту
+ Протокол Mux
+ Результат аналізу для маршрутизації
+ Результат аналізу для призначення
+ Визначати адресу сервера відповідно до політики IPv6
+ Автоматичний вибір програм для проксі
+ Автоматично вибрати програми для проксі, це очистить ваш поточний вибір.
+ Увімкнути ECH
+ Увімкнути Encrypted Client Hello
+ Налаштування ECH
+ Конфігурація ECH
+ Хост HTTPUpgrade
+ Шлях HTTPUpgrade
+ Оновити підписку поточної групи
+ Тип групи не є підпискою
+ Вимкнути перевірку сертифіката під час оновлення підписок
+ Завжди дозволяти незахищене
+ Мультиплексування
+ Заповнення (Padding)
+ Скидати вихідні з\'єднання при зміні мережі
+ Скидати вихідні з\'єднання при виході пристрою зі сну
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 2dbb97f45..a9f0c1cba 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -12,6 +12,8 @@
关于
未分组
切换
+ 启用
+ 禁用
分组名
更新
@@ -486,4 +488,15 @@
总是跳过 TLS 证书验证
当网络发生变化时重置出站连接
当设备从睡眠状态唤醒时重置出站连接
+ 本应用为预览版,可能存在诸多问题。若您不愿参与测试,请前往GitHub下载正式发布版本!
+ 检查预览版更新
+ 检查正式版更新
+ 发现新版本
+ 当前版本:%1$s\n可升级版本:%2$s\n是否前往下载?
+ 检查成功,但没有更新。
+ 恢复默认设置
+ 恢复默认设置,但节点、分组等数据将保留。如需完全清除数据,请在系统设置中直接清除应用数据。
+ 最小化
+ 无法读取已安装的应用。\n这通常是由于系统限制了应用的读取权限(例如某些中国厂商系统)。\n请在系统设置中授予权限。
+ 打开系统设置
\ No newline at end of file
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 840ee0f1c..bcf2c79dc 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -75,6 +75,8 @@
密碼(可選)
開始
切換
+ 啟用
+ 停用
編輯
WebSocket 設定
顯示直連速度
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 695e6cf58..de521bcbd 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -11,6 +11,8 @@
群組
關於
切換
+ 啟用
+ 停用
群組名稱
更新
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1899df1b4..1995ff051 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -17,6 +17,8 @@
Document
Ungrouped
Toggle
+ Enable
+ Disable
Switcher
Traffic
sing-box Dashboard
@@ -566,4 +568,15 @@
Padding
Reset outbound connections when network changes
Reset outbound connections when device wake from sleep
+ This application is a preview version and may contain many problems. If you do not want to test it, please go to GitHub to download the Release version!
+ Check for preview version updates
+ Check for release version updates
+ New version available
+ Current version: %1$s\nAvailable version: %2$s\nDo you want to download it?
+ Check successful, but no updates.
+ Restore default settings
+ Restore default settings, but data such as nodes and groups will be retained. To completely clear data, clear application data directly in the system settings.
+ Minimize
+ Unable to read installed apps.\nThis is usually because the system has restricted app read permissions.\nPlease grant permissions in the system settings.
+ Open System Settings
\ No newline at end of file
diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml
index 038587c2b..64f4f7901 100644
--- a/app/src/main/res/xml/global_preferences.xml
+++ b/app/src/main/res/xml/global_preferences.xml
@@ -29,7 +29,7 @@
app:title="@string/service_mode"
app:useSimpleSummaryProvider="true" />
+
@@ -176,6 +181,7 @@
app:summary="@string/dns_routing_message"
app:title="@string/enable_dns_routing" />
-
+
+
+
-
+
+
+
+
+
+
("
private lateinit var metadata: Properties
private lateinit var localProperties: Properties
-private lateinit var flavor: String
-
-fun Project.requireFlavor(): String {
- if (::flavor.isInitialized) return flavor
- if (gradle.startParameter.taskNames.isNotEmpty()) {
- val taskName = gradle.startParameter.taskNames[0]
- when {
- taskName.contains("assemble") -> {
- flavor = taskName.substringAfter("assemble")
- return flavor
- }
-
- taskName.contains("install") -> {
- flavor = taskName.substringAfter("install")
- return flavor
- }
-
- taskName.contains("bundle") -> {
- flavor = taskName.substringAfter("bundle")
- return flavor
- }
- }
- }
-
- flavor = ""
- return flavor
-}
fun Project.requireMetadata(): Properties {
if (!::metadata.isInitialized) {
@@ -156,8 +129,6 @@ fun Project.setupAppCommon() {
keyPassword = pwd
}
}
- } else if (requireFlavor().contains("(Oss|Expert|Play)Release".toRegex())) {
- exitProcess(0)
}
buildTypes {
val key = signingConfigs.findByName("release")
@@ -178,6 +149,7 @@ fun Project.setupApp() {
applicationId = pkgName
versionCode = verCode
versionName = verName
+ buildConfigField("String", "PRE_VERSION_NAME", "\"\"")
}
}
setupAppCommon()
@@ -209,14 +181,29 @@ fun Project.setupApp() {
create("oss")
create("fdroid")
create("play")
+ create("preview") {
+ buildConfigField(
+ "String",
+ "PRE_VERSION_NAME",
+ "\"${requireMetadata().getProperty("PRE_VERSION_NAME")}\""
+ )
+ }
}
applicationVariants.all {
outputs.all {
this as BaseVariantOutputImpl
- outputFileName = outputFileName.replace(project.name, "NekoBox-$versionName")
- .replace("-release", "")
- .replace("-oss", "")
+ val isPreview = outputFileName.contains("-preview")
+ outputFileName = if (isPreview) {
+ outputFileName.replace(
+ project.name,
+ "NekoBox-" + requireMetadata().getProperty("PRE_VERSION_NAME")
+ ).replace("-preview", "")
+ } else {
+ outputFileName.replace(project.name, "NekoBox-$versionName")
+ .replace("-release", "")
+ .replace("-oss", "")
+ }
}
}
diff --git a/libcore/box.go b/libcore/box.go
index 9caccdc3c..5df6d4ba0 100644
--- a/libcore/box.go
+++ b/libcore/box.go
@@ -14,6 +14,7 @@ import (
"github.com/matsuridayo/libneko/protect_server"
"github.com/matsuridayo/libneko/speedtest"
+ "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/boxapi"
"github.com/sagernet/sing-box/experimental/libbox/platform"
"github.com/sagernet/sing-box/protocol/group"
@@ -78,12 +79,15 @@ type BoxInstance struct {
pauseManager pause.Manager
}
-func NewSingBoxInstance(config string) (b *BoxInstance, err error) {
+func NewSingBoxInstance(config string, localTransport LocalDNSTransport) (b *BoxInstance, err error) {
defer device.DeferPanicToError("NewSingBoxInstance", func(err_ error) { err = err_ })
// create box context
ctx, cancel := context.WithCancel(context.Background())
- ctx = box.Context(ctx, nekoboxAndroidInboundRegistry(), nekoboxAndroidOutboundRegistry(), nekoboxAndroidEndpointRegistry())
+ ctx = box.Context(ctx,
+ nekoboxAndroidInboundRegistry(), nekoboxAndroidOutboundRegistry(), nekoboxAndroidEndpointRegistry(),
+ nekoboxAndroidDNSTransportRegistry(localTransport), nekoboxAndroidServiceRegistry(),
+ )
ctx = service.ContextWithDefaultRegistry(ctx)
service.MustRegister[platform.Interface](ctx, boxPlatformInterfaceInstance)
@@ -182,11 +186,17 @@ func (b *BoxInstance) SetAsMain() {
}
func (b *BoxInstance) SetV2rayStats(outbounds string) {
+ b.access.Lock()
+ defer b.access.Unlock()
+ if b.v2api != nil {
+ log.Println("duplicate call of SetV2rayStats")
+ return
+ }
b.v2api = boxapi.NewSbV2rayServer(option.V2RayStatsServiceOptions{
Enabled: true,
Outbounds: strings.Split(outbounds, "\n"),
})
- b.Box.Router().SetNekoTracker(b.v2api.StatsService())
+ b.Box.Router().AppendTracker(b.v2api.StatsService())
}
func (b *BoxInstance) QueryStats(tag, direct string) int64 {
@@ -205,11 +215,23 @@ func (b *BoxInstance) SelectOutbound(tag string) bool {
func UrlTest(i *BoxInstance, link string, timeout int32) (latency int32, err error) {
defer device.DeferPanicToError("box.UrlTest", func(err_ error) { err = err_ })
- if i == nil {
- // test current
- return speedtest.UrlTest(boxapi.CreateProxyHttpClient(mainInstance.Box), link, timeout, speedtest.UrlTestStandard_RTT)
+ var connectionTracker adapter.ConnectionTracker
+ // test i
+ if i != nil {
+ if i.v2api != nil {
+ connectionTracker = i.v2api.StatsService()
+ }
+ return speedtest.UrlTest(boxapi.CreateProxyHttpClient(i.Box, connectionTracker), link, timeout, speedtest.UrlTestStandard_RTT)
+ }
+ // test direct
+ if mainInstance == nil {
+ return speedtest.UrlTest(boxapi.CreateProxyHttpClient(nil, nil), link, timeout, speedtest.UrlTestStandard_RTT)
+ }
+ // test mainInstance
+ if mainInstance.v2api != nil {
+ connectionTracker = mainInstance.v2api.StatsService()
}
- return speedtest.UrlTest(boxapi.CreateProxyHttpClient(i.Box), link, timeout, speedtest.UrlTestStandard_RTT)
+ return speedtest.UrlTest(boxapi.CreateProxyHttpClient(mainInstance.Box, connectionTracker), link, timeout, speedtest.UrlTestStandard_RTT)
}
var protectCloser io.Closer
diff --git a/libcore/box_include.go b/libcore/box_include.go
index c57fd99bc..5a95cd867 100644
--- a/libcore/box_include.go
+++ b/libcore/box_include.go
@@ -1,13 +1,25 @@
package libcore
import (
+ "context"
+
+ "github.com/sagernet/sing-box/adapter"
"github.com/sagernet/sing-box/adapter/endpoint"
"github.com/sagernet/sing-box/adapter/inbound"
"github.com/sagernet/sing-box/adapter/outbound"
+ "github.com/sagernet/sing-box/adapter/service"
+ "github.com/sagernet/sing-box/dns"
+ "github.com/sagernet/sing-box/dns/transport"
+ "github.com/sagernet/sing-box/dns/transport/fakeip"
+ "github.com/sagernet/sing-box/dns/transport/hosts"
+ "github.com/sagernet/sing-box/dns/transport/local"
+ "github.com/sagernet/sing-box/dns/transport/quic"
+ "github.com/sagernet/sing-box/log"
+ "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing-box/protocol/anytls"
"github.com/sagernet/sing-box/protocol/block"
"github.com/sagernet/sing-box/protocol/direct"
- "github.com/sagernet/sing-box/protocol/dns"
+ protocolDns "github.com/sagernet/sing-box/protocol/dns"
"github.com/sagernet/sing-box/protocol/group"
"github.com/sagernet/sing-box/protocol/http"
"github.com/sagernet/sing-box/protocol/hysteria"
@@ -28,7 +40,6 @@ import (
_ "github.com/sagernet/sing-box/experimental/clashapi"
_ "github.com/sagernet/sing-box/transport/v2rayquic"
- _ "github.com/sagernet/sing-dns/quic"
)
func nekoboxAndroidInboundRegistry() *inbound.Registry {
@@ -52,7 +63,7 @@ func nekoboxAndroidOutboundRegistry() *outbound.Registry {
direct.RegisterOutbound(registry)
block.RegisterOutbound(registry)
- dns.RegisterOutbound(registry)
+ protocolDns.RegisterOutbound(registry)
group.RegisterSelector(registry)
group.RegisterURLTest(registry)
@@ -68,8 +79,11 @@ func nekoboxAndroidOutboundRegistry() *outbound.Registry {
vless.RegisterOutbound(registry)
anytls.RegisterOutbound(registry)
- registerQUICOutbounds(registry)
- registerWireGuardOutbound(registry)
+ hysteria.RegisterOutbound(registry)
+ tuic.RegisterOutbound(registry)
+ hysteria2.RegisterOutbound(registry)
+
+ wireguard.RegisterOutbound(registry)
return registry
}
@@ -77,27 +91,38 @@ func nekoboxAndroidOutboundRegistry() *outbound.Registry {
func nekoboxAndroidEndpointRegistry() *endpoint.Registry {
registry := endpoint.NewRegistry()
- registerWireGuardEndpoint(registry)
+ wireguard.RegisterEndpoint(registry)
return registry
}
-func registerQUICInbounds(registry *inbound.Registry) {
- hysteria.RegisterInbound(registry)
- tuic.RegisterInbound(registry)
- hysteria2.RegisterInbound(registry)
-}
+func nekoboxAndroidDNSTransportRegistry(localTransport LocalDNSTransport) *dns.TransportRegistry {
+ registry := dns.NewTransportRegistry()
-func registerQUICOutbounds(registry *outbound.Registry) {
- hysteria.RegisterOutbound(registry)
- tuic.RegisterOutbound(registry)
- hysteria2.RegisterOutbound(registry)
-}
+ transport.RegisterTCP(registry)
+ transport.RegisterUDP(registry)
+ transport.RegisterTLS(registry)
+ transport.RegisterHTTPS(registry)
+ hosts.RegisterTransport(registry)
+ // local.RegisterTransport(registry)
+ fakeip.RegisterTransport(registry)
-func registerWireGuardOutbound(registry *outbound.Registry) {
- wireguard.RegisterOutbound(registry)
+ quic.RegisterTransport(registry)
+ quic.RegisterHTTP3Transport(registry)
+
+ if localTransport == nil {
+ local.RegisterTransport(registry)
+ } else {
+ dns.RegisterTransport(registry, "local", func(ctx context.Context, logger log.ContextLogger, tag string, options option.LocalDNSServerOptions) (adapter.DNSTransport, error) {
+ return newPlatformTransport(localTransport, tag, options), nil
+ })
+ }
+
+ return registry
}
-func registerWireGuardEndpoint(registry *endpoint.Registry) {
- wireguard.RegisterEndpoint(registry)
+func nekoboxAndroidServiceRegistry() *service.Registry {
+ registry := service.NewRegistry()
+
+ return registry
}
diff --git a/libcore/build.sh b/libcore/build.sh
index a6b90cd09..233e89d4d 100755
--- a/libcore/build.sh
+++ b/libcore/build.sh
@@ -14,7 +14,8 @@ if [ -z "$GOPATH" ]; then
GOPATH=$(go env GOPATH)
fi
-"$GOPATH"/bin/gomobile-matsuri bind -v -androidapi 21 -cache "$(realpath $BUILD)" -trimpath -ldflags='-s -w' -tags='with_conntrack,with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api,with_ech' . || exit 1
+export GOBIND=gobind-matsuri
+"$GOPATH"/bin/gomobile-matsuri bind -v -androidapi 21 -cache "$(realpath $BUILD)" -trimpath -ldflags='-s -w' -tags='with_conntrack,with_gvisor,with_quic,with_wireguard,with_utls,with_clash_api' . || exit 1
rm -r libcore-sources.jar
proj=../app/libs
diff --git a/libcore/device/debug.go b/libcore/device/debug.go
index 016c17fe9..624bdfaae 100644
--- a/libcore/device/debug.go
+++ b/libcore/device/debug.go
@@ -13,9 +13,11 @@ func GoDebug(any interface{}) {
}
}
-func DeferPanicToError(name string, err func(error)) {
+func DeferPanicToError(name string, onError func(error)) {
if r := recover(); r != nil {
- s := fmt.Errorf("%s panic: %s\n%s", name, r, string(debug.Stack()))
- err(s)
+ if onError != nil {
+ s := fmt.Errorf("%s panic: %s\n%s", name, r, string(debug.Stack()))
+ onError(s)
+ }
}
}
diff --git a/libcore/dns_android.go b/libcore/dns_android.go
new file mode 100644
index 000000000..184c4ab6f
--- /dev/null
+++ b/libcore/dns_android.go
@@ -0,0 +1,114 @@
+//go:build android && cgo
+
+package libcore
+
+/*
+#include
+#include
+#include
+#include
+
+typedef int (*android_res_nsend_t)(uint64_t network, const uint8_t* msg, size_t msglen, int flags);
+typedef int (*android_res_nresult_t)(int fd, int* rcode, uint8_t* resp, size_t resp_len);
+
+static int call_android_res_nsend(void* sym, uint64_t network, const uint8_t* msg, size_t msglen, int flags) {
+ android_res_nsend_t f = (android_res_nsend_t)sym;
+ if (!f) return -1;
+ return f(network, msg, msglen, flags);
+}
+
+static int call_android_res_nresult(void* sym, int fd, int* rcode, uint8_t* resp, size_t resp_len) {
+ android_res_nresult_t f = (android_res_nresult_t)sym;
+ if (!f) return -1;
+ return f(fd, rcode, resp, resp_len);
+}
+*/
+import "C"
+
+import (
+ "context"
+ "errors"
+ "os"
+ "unsafe"
+
+ "golang.org/x/sys/unix"
+)
+
+func init() {
+ libname := C.CString("libandroid.so")
+ defer C.free(unsafe.Pointer(libname))
+
+ libHandle := C.dlopen(libname, C.int(C.RTLD_NOW))
+ if libHandle == nil {
+ return
+ }
+
+ symNameSend := C.CString("android_res_nsend")
+ defer C.free(unsafe.Pointer(symNameSend))
+ androidResNSendSym := C.dlsym(libHandle, symNameSend)
+ if androidResNSendSym == nil {
+ return
+ }
+
+ symNameResult := C.CString("android_res_nresult")
+ defer C.free(unsafe.Pointer(symNameResult))
+ androidResNResultSym := C.dlsym(libHandle, symNameResult)
+ if androidResNResultSym == nil {
+ return
+ }
+
+ callAndroidResNSend := func(network uint64, msg []byte) (int, error) {
+ if len(msg) == 0 {
+ return 0, errors.New("empty payload")
+ }
+ msgPtr := (*C.uint8_t)(unsafe.Pointer(&msg[0]))
+ msgLen := C.size_t(len(msg))
+ ret := C.call_android_res_nsend(androidResNSendSym, C.uint64_t(network), msgPtr, msgLen, C.int(0))
+ return int(ret), nil
+ }
+
+ callAndroidResNResult := func(fd int, resp []byte) (int, int) {
+ if len(resp) == 0 {
+ return 0, 0
+ }
+ respPtr := (*C.uint8_t)(unsafe.Pointer(&resp[0]))
+ respLen := C.size_t(len(resp))
+ var rcode C.int
+ n := C.call_android_res_nresult(androidResNResultSym, C.int(fd), &rcode, respPtr, respLen)
+ return int(rcode), int(n)
+ }
+
+ // set rawQueryFunc
+ rawQueryFunc = func(networkHandle int64, request []byte) ([]byte, error) {
+ fd, err := callAndroidResNSend(uint64(networkHandle), request)
+ if err != nil {
+ return nil, err
+ }
+ if fd < 0 {
+ return nil, unix.Errno(-fd)
+ }
+
+ // wait for response (timeout 5000 ms)
+ pfds := []unix.PollFd{{Fd: int32(fd), Events: unix.POLLIN | unix.POLLERR}}
+ nReady, err := unix.Poll(pfds, 5000)
+ if err != nil {
+ unix.Close(fd)
+ return nil, err
+ }
+ if nReady == 0 {
+ unix.Close(fd)
+ return nil, context.DeadlineExceeded
+ }
+
+ // read response into buffer
+ response := make([]byte, 8192)
+ _, n := callAndroidResNResult(fd, response)
+ if n < 0 {
+ return nil, unix.Errno(-n)
+ }
+ if n == 0 {
+ return nil, os.ErrInvalid
+ }
+ return response[:n], nil
+ }
+}
diff --git a/libcore/dns_box.go b/libcore/dns_box.go
index c7d244767..3f1cd5fbb 100644
--- a/libcore/dns_box.go
+++ b/libcore/dns_box.go
@@ -6,9 +6,13 @@ import (
"context"
"net/netip"
"strings"
+ "sync"
"syscall"
- dns "github.com/sagernet/sing-dns"
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/constant"
+ "github.com/sagernet/sing-box/dns"
+ "github.com/sagernet/sing-box/option"
"github.com/sagernet/sing/common"
E "github.com/sagernet/sing/common/exceptions"
M "github.com/sagernet/sing/common/metadata"
@@ -17,110 +21,103 @@ import (
mDNS "github.com/miekg/dns"
)
+var rawQueryFunc func(networkHandle int64, request []byte) ([]byte, error)
+
type LocalDNSTransport interface {
Raw() bool
+ NetworkHandle() int64
Lookup(ctx *ExchangeContext, network string, domain string) error
Exchange(ctx *ExchangeContext, message []byte) error
}
-func RegisterLocalDNSTransport(transport LocalDNSTransport) {
- if transport == nil {
- dns.RegisterTransport([]string{"local"}, dns.CreateTransport)
- } else {
- dns.RegisterTransport([]string{"local"}, func(options dns.TransportOptions) (dns.Transport, error) {
- return &platformLocalDNSTransport{
- iif: transport,
- }, nil
- })
- }
-}
-
-var _ dns.Transport = (*platformLocalDNSTransport)(nil)
+var gLocalDNSTransport *platformLocalDNSTransport = nil
type platformLocalDNSTransport struct {
+ dns.TransportAdapter
iif LocalDNSTransport
+ raw bool
}
-func (p *platformLocalDNSTransport) Name() string {
- return "local"
+func newPlatformTransport(iif LocalDNSTransport, tag string, options option.LocalDNSServerOptions) *platformLocalDNSTransport {
+ return &platformLocalDNSTransport{
+ TransportAdapter: dns.NewTransportAdapterWithLocalOptions(constant.DNSTypeLocal, tag, options),
+ iif: iif,
+ raw: iif.Raw(),
+ }
}
-func (p *platformLocalDNSTransport) Start() error {
+func (p *platformLocalDNSTransport) Start(stage adapter.StartStage) error {
return nil
}
-func (p *platformLocalDNSTransport) Reset() {
-}
-
func (p *platformLocalDNSTransport) Close() error {
return nil
}
-func (p *platformLocalDNSTransport) Raw() bool {
- return p.iif.Raw()
-}
-
func (p *platformLocalDNSTransport) Exchange(ctx context.Context, message *mDNS.Msg) (*mDNS.Msg, error) {
- messageBytes, err := message.Pack()
- if err != nil {
- return nil, err
- }
- response := &ExchangeContext{
- context: ctx,
- }
- var responseMessage *mDNS.Msg
- return responseMessage, task.Run(ctx, func() error {
- err = p.iif.Exchange(response, messageBytes)
+ if p.raw && rawQueryFunc != nil {
+ // Raw - Android 10 及以上才有
+
+ messageBytes, err := message.Pack()
if err != nil {
- return err
- }
- if response.error != nil {
- return response.error
+ return nil, err
}
- responseMessage = &response.message
- return nil
- })
-}
-
-func (p *platformLocalDNSTransport) Lookup(ctx context.Context, domain string, strategy dns.DomainStrategy) ([]netip.Addr, error) {
- var network string
- switch strategy {
- case dns.DomainStrategyUseIPv4:
- network = "ip4"
- case dns.DomainStrategyPreferIPv6:
- network = "ip6"
- default:
- network = "ip"
- }
- response := &ExchangeContext{
- context: ctx,
- }
- var responseAddr []netip.Addr
- return responseAddr, task.Run(ctx, func() error {
- err := p.iif.Lookup(response, network, domain)
+ msg, err := rawQueryFunc(p.iif.NetworkHandle(), messageBytes)
if err != nil {
- return err
+ return nil, err
}
- if response.error != nil {
- return response.error
+ responseMessage := new(mDNS.Msg)
+ err = responseMessage.Unpack(msg)
+ if err != nil {
+ return nil, err
}
- switch strategy {
- case dns.DomainStrategyUseIPv4:
- responseAddr = common.Filter(response.addresses, func(it netip.Addr) bool {
- return it.Is4()
- })
- case dns.DomainStrategyPreferIPv6:
- responseAddr = common.Filter(response.addresses, func(it netip.Addr) bool {
- return it.Is6()
- })
+ return responseMessage, nil
+ } else {
+ // Lookup - Android 10 以下
+
+ question := message.Question[0]
+ var network string
+ switch question.Qtype {
+ case mDNS.TypeA:
+ network = "ip4"
+ case mDNS.TypeAAAA:
+ network = "ip6"
default:
- responseAddr = response.addresses
+ return nil, E.New("only IP queries are supported by current version of Android")
}
- /*if len(responseAddr) == 0 {
- response.error = dns.RCodeSuccess
- }*/
- return nil
- })
+
+ done := make(chan struct{})
+ response := &ExchangeContext{
+ context: ctx,
+ done: sync.OnceFunc(func() {
+ close(done)
+ }),
+ }
+
+ var responseAddrs []netip.Addr
+ var group task.Group
+ group.Append0(func(ctx context.Context) error {
+ err := p.iif.Lookup(response, network, question.Name)
+ if err != nil {
+ return err
+ }
+ select {
+ case <-done:
+ case <-ctx.Done():
+ return context.Canceled
+ }
+ if response.error != nil {
+ return response.error
+ }
+ responseAddrs = response.addresses
+ return nil
+ })
+ err := group.Run(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return dns.FixedResponse(message.Id, question, responseAddrs, constant.DefaultDNSTTL), nil
+ }
}
type Func interface {
@@ -132,6 +129,7 @@ type ExchangeContext struct {
message mDNS.Msg
addresses []netip.Addr
error error
+ done func()
}
func (c *ExchangeContext) OnCancel(callback Func) {
@@ -154,12 +152,15 @@ func (c *ExchangeContext) RawSuccess(result []byte) {
if err != nil {
c.error = E.Cause(err, "parse response")
}
+ c.done()
}
func (c *ExchangeContext) ErrorCode(code int32) {
- c.error = dns.RCodeError(code)
+ c.error = dns.RcodeError(code)
+ c.done()
}
func (c *ExchangeContext) ErrnoCode(code int32) {
c.error = syscall.Errno(code)
+ c.done()
}
diff --git a/libcore/ech/ech.go b/libcore/ech/ech.go
new file mode 100644
index 000000000..0094208de
--- /dev/null
+++ b/libcore/ech/ech.go
@@ -0,0 +1,79 @@
+package ech
+
+import (
+ "context"
+ "crypto/tls"
+ "encoding/base64"
+ "log"
+ "net"
+ "os"
+
+ mDNS "github.com/miekg/dns"
+ "github.com/sagernet/sing-box/adapter"
+ "github.com/sagernet/sing-box/dns"
+ "github.com/sagernet/sing/common/exceptions"
+)
+
+type ECHClientConfig struct {
+ *tls.Config
+ domain string
+ localDnsTransport adapter.DNSTransport
+}
+
+func NewECHClientConfig(domain string, tlsConfig *tls.Config, localDnsTransport adapter.DNSTransport) *ECHClientConfig {
+ config := tlsConfig.Clone()
+ config.ServerName = domain
+ return &ECHClientConfig{
+ Config: config,
+ domain: domain,
+ localDnsTransport: localDnsTransport,
+ }
+}
+
+func (s *ECHClientConfig) Client(ctx context.Context, conn net.Conn) (*tls.Conn, error) {
+ err := s.fetchEchKeys(ctx, conn)
+ if err != nil {
+ // allow empty ech keys
+ log.Println("fetchEchKeys:", err)
+ }
+ return tls.Client(conn, s.Config), nil
+}
+
+func (s *ECHClientConfig) fetchEchKeys(ctx context.Context, conn net.Conn) error {
+ message := &mDNS.Msg{
+ MsgHdr: mDNS.MsgHdr{
+ RecursionDesired: true,
+ },
+ Question: []mDNS.Question{
+ {
+ Name: mDNS.Fqdn(s.domain),
+ Qtype: mDNS.TypeHTTPS,
+ Qclass: mDNS.ClassINET,
+ },
+ },
+ }
+ if s.localDnsTransport == nil {
+ return os.ErrInvalid
+ }
+ response, err := s.localDnsTransport.Exchange(ctx, message)
+ if err != nil {
+ return exceptions.Cause(err, "fetch ECH config list")
+ }
+ if response.Rcode != mDNS.RcodeSuccess {
+ return exceptions.Cause(dns.RcodeError(response.Rcode), "fetch ECH config list")
+ }
+ for _, rr := range response.Answer {
+ switch resource := rr.(type) {
+ case *mDNS.HTTPS:
+ for _, value := range resource.Value {
+ if value.Key().String() == "ech" {
+ echConfigList, err := base64.StdEncoding.DecodeString(value.String())
+ if err == nil {
+ s.Config.EncryptedClientHelloConfigList = echConfigList
+ }
+ }
+ }
+ }
+ }
+ return nil
+}
diff --git a/libcore/go.mod b/libcore/go.mod
index 4adc1b568..ddf89a1f6 100644
--- a/libcore/go.mod
+++ b/libcore/go.mod
@@ -6,95 +6,82 @@ toolchain go1.23.6
require (
github.com/matsuridayo/libneko v1.0.0 // replaced
- github.com/miekg/dns v1.1.63
- github.com/oschwald/maxminddb-golang v1.12.0
- github.com/sagernet/sing v0.6.6-0.20250406121928-926a5a1e8bb7
+ github.com/miekg/dns v1.1.67
+ github.com/oschwald/maxminddb-golang v1.13.1
+ github.com/sagernet/quic-go v0.52.0-sing-box-mod.3
+ github.com/sagernet/sing v0.7.18
github.com/sagernet/sing-box v1.0.0 // replaced
- github.com/sagernet/sing-dns v0.4.1
- github.com/sagernet/sing-tun v0.6.1
- github.com/ulikunitz/xz v0.5.11
+ github.com/sagernet/sing-tun v0.7.10
+ github.com/ulikunitz/xz v0.5.15
golang.org/x/mobile v0.0.0-20231108233038-35478a0c49da
- golang.org/x/sys v0.30.0
+ golang.org/x/sys v0.35.0
)
require (
github.com/ajg/form v1.5.1 // indirect
- github.com/andybalholm/brotli v1.0.6 // indirect
- github.com/anytls/sing-anytls v0.0.8 // indirect
- github.com/caddyserver/certmagic v0.20.0 // indirect
- github.com/cloudflare/circl v1.3.7 // indirect
+ github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/anytls/sing-anytls v0.0.11 // indirect
+ github.com/caddyserver/certmagic v0.23.0 // indirect
+ github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cretz/bine v0.2.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
- github.com/go-chi/chi/v5 v5.2.1 // indirect
+ github.com/go-chi/chi/v5 v5.2.2 // indirect
github.com/go-chi/render v1.0.3 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
- github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gofrs/uuid/v5 v5.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
- github.com/google/go-cmp v0.6.0 // indirect
- github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
+ github.com/google/go-cmp v0.7.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
- github.com/josharian/native v1.1.0 // indirect
- github.com/klauspost/compress v1.17.4 // indirect
- github.com/klauspost/cpuid/v2 v2.2.5 // indirect
- github.com/libdns/alidns v1.0.3 // indirect
- github.com/libdns/cloudflare v0.1.1 // indirect
- github.com/libdns/libdns v0.2.2 // indirect
+ github.com/klauspost/compress v1.17.11 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/libdns/alidns v1.0.5-libdns.v1.beta1 // indirect
+ github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 // indirect
+ github.com/libdns/libdns v1.1.0 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
- github.com/mdlayher/netlink v1.7.2 // indirect
- github.com/mdlayher/socket v0.4.1 // indirect
- github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 // indirect
- github.com/mholt/acmez v1.2.0 // indirect
- github.com/onsi/ginkgo/v2 v2.9.7 // indirect
- github.com/quic-go/qpack v0.4.0 // indirect
- github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
+ github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 // indirect
+ github.com/mdlayher/socket v0.5.1 // indirect
+ github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 // indirect
+ github.com/metacubex/utls v1.8.4 // indirect
+ github.com/mholt/acmez/v3 v3.1.2 // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
- github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 // indirect
github.com/sagernet/cors v1.2.1 // indirect
github.com/sagernet/fswatch v0.1.1 // indirect
- github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff // indirect
+ github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb // indirect
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
github.com/sagernet/nftables v0.3.0-beta.4 // indirect
- github.com/sagernet/quic-go v0.49.0-beta.1 // indirect
- github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 // indirect
- github.com/sagernet/sing-mux v0.3.1 // indirect
- github.com/sagernet/sing-quic v0.4.1 // indirect
- github.com/sagernet/sing-shadowsocks v0.2.7 // indirect
- github.com/sagernet/sing-shadowsocks2 v0.2.0 // indirect
- github.com/sagernet/sing-shadowtls v0.2.0 // indirect
- github.com/sagernet/sing-vmess v0.2.0 // indirect
- github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
- github.com/sagernet/utls v1.6.7 // indirect
- github.com/sagernet/wireguard-go v0.0.1-beta.5 // indirect
+ github.com/sagernet/sing-mux v0.3.4 // indirect
+ github.com/sagernet/sing-quic v0.5.2 // indirect
+ github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
+ github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect
+ github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect
+ github.com/sagernet/sing-vmess v0.2.7 // indirect
+ github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
+ github.com/sagernet/wireguard-go v0.0.1-beta.7 // indirect
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
- github.com/vishvananda/netns v0.0.4 // indirect
- github.com/zeebo/blake3 v0.2.3 // indirect
+ github.com/vishvananda/netns v0.0.5 // indirect
+ github.com/zeebo/blake3 v0.2.4 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
+ go.uber.org/zap/exp v0.3.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
- golang.org/x/crypto v0.32.0 // indirect
- golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
- golang.org/x/mod v0.20.0 // indirect
- golang.org/x/net v0.34.0 // indirect
- golang.org/x/sync v0.10.0 // indirect
- golang.org/x/text v0.21.0 // indirect
- golang.org/x/time v0.7.0 // indirect
- golang.org/x/tools v0.24.0 // indirect
+ golang.org/x/crypto v0.41.0 // indirect
+ golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
+ golang.org/x/mod v0.27.0 // indirect
+ golang.org/x/net v0.43.0 // indirect
+ golang.org/x/sync v0.16.0 // indirect
+ golang.org/x/text v0.28.0 // indirect
+ golang.org/x/time v0.9.0 // indirect
+ golang.org/x/tools v0.36.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
- google.golang.org/grpc v1.63.2 // indirect
- google.golang.org/protobuf v1.33.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
+ google.golang.org/grpc v1.73.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)
replace github.com/matsuridayo/libneko => ../../libneko
replace github.com/sagernet/sing-box => ../../sing-box
-
-// replace github.com/sagernet/sing-quic => ../../sing-quic
-
-// replace github.com/sagernet/sing => ../../sing
-
-// replace github.com/sagernet/sing-dns => ../../sing-dns
diff --git a/libcore/go.sum b/libcore/go.sum
index 3fde62ae6..dcef8d1db 100644
--- a/libcore/go.sum
+++ b/libcore/go.sum
@@ -1,30 +1,30 @@
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
-github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
-github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/anytls/sing-anytls v0.0.8 h1:1u/fnH1HoeeMV5mX7/eUOjLBvPdkd1UJRmXiRi6Vymc=
-github.com/anytls/sing-anytls v0.0.8/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
-github.com/caddyserver/certmagic v0.20.0 h1:bTw7LcEZAh9ucYCRXyCpIrSAGplplI0vGYJ4BpCQ/Fc=
-github.com/caddyserver/certmagic v0.20.0/go.mod h1:N4sXgpICQUskEWpj7zVzvWD41p3NYacrNoZYiRM2jTg=
-github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
-github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
+github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
+github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
+github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
+github.com/caddyserver/certmagic v0.23.0 h1:CfpZ/50jMfG4+1J/u2LV6piJq4HOfO6ppOnOf7DkFEU=
+github.com/caddyserver/certmagic v0.23.0/go.mod h1:9mEZIWqqWoI+Gf+4Trh04MOVPD0tGSxtqsxg87hAIH4=
+github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA=
+github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
-github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
+github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
+github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
@@ -35,161 +35,157 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
-github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
-github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
-github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
-github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
-github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
-github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
-github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
-github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
-github.com/libdns/alidns v1.0.3 h1:LFHuGnbseq5+HCeGa1aW8awyX/4M2psB9962fdD2+yQ=
-github.com/libdns/alidns v1.0.3/go.mod h1:e18uAG6GanfRhcJj6/tps2rCMzQJaYVcGKT+ELjdjGE=
-github.com/libdns/cloudflare v0.1.1 h1:FVPfWwP8zZCqj268LZjmkDleXlHPlFU9KC4OJ3yn054=
-github.com/libdns/cloudflare v0.1.1/go.mod h1:9VK91idpOjg6v7/WbjkEW49bSCxj00ALesIFDhJ8PBU=
-github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
-github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
-github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/libdns/alidns v1.0.5-libdns.v1.beta1 h1:txHK7UxDed3WFBDjrTZPuMn8X+WmhjBTTAMW5xdy5pQ=
+github.com/libdns/alidns v1.0.5-libdns.v1.beta1/go.mod h1:ystHmPwcGoWjPrGpensQSMY9VoCx4cpR2hXNlwk9H/g=
+github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6 h1:3MGrVWs2COjMkQR17oUw1zMIPbm2YAzxDC3oGVZvQs8=
+github.com/libdns/cloudflare v0.2.2-0.20250708034226-c574dccb31a6/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
+github.com/libdns/libdns v1.0.0-beta.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
+github.com/libdns/libdns v1.1.0 h1:9ze/tWvt7Df6sbhOJRB8jT33GHEHpEQXdtkE3hPthbU=
+github.com/libdns/libdns v1.1.0/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
-github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
-github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
-github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
-github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
-github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422 h1:zGeQt3UyNydIVrMRB97AA5WsYEau/TyCnRtTf1yUmJY=
-github.com/metacubex/tfo-go v0.0.0-20241231083714-66613d49c422/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
-github.com/mholt/acmez v1.2.0 h1:1hhLxSgY5FvH5HCnGUuwbKY2VQVo8IU7rxXKSnZ7F30=
-github.com/mholt/acmez v1.2.0/go.mod h1:VT9YwH1xgNX1kmYY89gY8xPJC84BFAisjo8Egigt4kE=
-github.com/miekg/dns v1.1.63 h1:8M5aAw6OMZfFXTT7K5V0Eu5YiiL8l7nUAkyN6C9YwaY=
-github.com/miekg/dns v1.1.63/go.mod h1:6NGHfjhpmr5lt3XPLuyfDJi5AXbNIPM9PY6H6sF1Nfs=
-github.com/onsi/ginkgo/v2 v2.9.7 h1:06xGQy5www2oN160RtEZoTvnP2sPhEfePYmCDc2szss=
-github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0=
-github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
-github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4=
-github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
-github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42 h1:A1Cq6Ysb0GM0tpKMbdCXCIfBclan4oHk1Jb+Hrejirg=
+github.com/mdlayher/netlink v1.7.3-0.20250113171957-fbb4dce95f42/go.mod h1:BB4YCPDOzfy7FniQ/lxuYQ3dgmM2cZumHbK8RpTjN2o=
+github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
+github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
+github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0 h1:Ui+/2s5Qz0lSnDUBmEL12M5Oi/PzvFxGTNohm8ZcsmE=
+github.com/metacubex/tfo-go v0.0.0-20250921095601-b102db4216c0/go.mod h1:l9oLnLoEXyGZ5RVLsh7QCC5XsouTUyKk4F2nLm2DHLw=
+github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
+github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
+github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
+github.com/mholt/acmez/v3 v3.1.2/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ=
+github.com/miekg/dns v1.1.67 h1:kg0EHj0G4bfT5/oOys6HhZw4vmMlnoZ+gDu8tJ/AlI0=
+github.com/miekg/dns v1.1.67/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
+github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
+github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
-github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
-github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1 h1:YbmpqPQEMdlk9oFSKYWRqVuu9qzNiOayIonKmv1gCXY=
-github.com/sagernet/cloudflare-tls v0.0.0-20231208171750-a4483c1b7cd1/go.mod h1:J2yAxTFPDjrDPhuAi9aWFz2L3ox9it4qAluBBbN0H5k=
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
github.com/sagernet/fswatch v0.1.1 h1:YqID+93B7VRfqIH3PArW/XpJv5H4OLEVWDfProGoRQs=
github.com/sagernet/fswatch v0.1.1/go.mod h1:nz85laH0mkQqJfaOrqPpkwtU1znMFNVTpT/5oRsVz/o=
-github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff h1:mlohw3360Wg1BNGook/UHnISXhUx4Gd/3tVLs5T0nSs=
-github.com/sagernet/gvisor v0.0.0-20241123041152-536d05261cff/go.mod h1:ehZwnT2UpmOWAHFL48XdBhnd4Qu4hN2O3Ji0us3ZHMw=
+github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb h1:pprQtDqNgqXkRsXn+0E8ikKOemzmum8bODjSfDene38=
+github.com/sagernet/gvisor v0.0.0-20250325023245-7a9c0f5725fb/go.mod h1:QkkPEJLw59/tfxgapHta14UL5qMUah5NXhO0Kw2Kan4=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
github.com/sagernet/nftables v0.3.0-beta.4 h1:kbULlAwAC3jvdGAC1P5Fa3GSxVwQJibNenDW2zaXr8I=
github.com/sagernet/nftables v0.3.0-beta.4/go.mod h1:OQXAjvjNGGFxaTgVCSTRIhYB5/llyVDeapVoENYBDS8=
-github.com/sagernet/quic-go v0.49.0-beta.1 h1:3LdoCzVVfYRibZns1tYWSIoB65fpTmrwy+yfK8DQ8Jk=
-github.com/sagernet/quic-go v0.49.0-beta.1/go.mod h1:uesWD1Ihrldq1M3XtjuEvIUqi8WHNsRs71b3Lt1+p/U=
-github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691 h1:5Th31OC6yj8byLGkEnIYp6grlXfo1QYUfiYFGjewIdc=
-github.com/sagernet/reality v0.0.0-20230406110435-ee17307e7691/go.mod h1:B8lp4WkQ1PwNnrVMM6KyuFR20pU8jYBD+A4EhJovEXU=
-github.com/sagernet/sing v0.2.18/go.mod h1:OL6k2F0vHmEzXz2KW19qQzu172FDgSbUSODylighuVo=
-github.com/sagernet/sing v0.6.6-0.20250406121928-926a5a1e8bb7 h1:ZJauxLmH12Gzv3nucfjsSBQw9UA8t7Sxu8pYHBSP2TU=
-github.com/sagernet/sing v0.6.6-0.20250406121928-926a5a1e8bb7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
-github.com/sagernet/sing-dns v0.4.1 h1:nozS7iqpxZ7aV73oHbkD/8haOvf3XXDCgT//8NdYirk=
-github.com/sagernet/sing-dns v0.4.1/go.mod h1:dweQs54ng2YGzoJfz+F9dGuDNdP5pJ3PLeggnK5VWc8=
-github.com/sagernet/sing-mux v0.3.1 h1:kvCc8HyGAskDHDQ0yQvoTi/7J4cZPB/VJMsAM3MmdQI=
-github.com/sagernet/sing-mux v0.3.1/go.mod h1:Mkdz8LnDstthz0HWuA/5foncnDIdcNN5KZ6AdJX+x78=
-github.com/sagernet/sing-quic v0.4.1 h1:pxlMa4efZu/M07RgGagNNDDyl6ZUwpmNUjRTpgHOWK4=
-github.com/sagernet/sing-quic v0.4.1/go.mod h1:tqPa0/Wqa19MkkSlKVZZX5sHxtiDR9BROcn4ufcbVdY=
-github.com/sagernet/sing-shadowsocks v0.2.7 h1:zaopR1tbHEw5Nk6FAkM05wCslV6ahVegEZaKMv9ipx8=
-github.com/sagernet/sing-shadowsocks v0.2.7/go.mod h1:0rIKJZBR65Qi0zwdKezt4s57y/Tl1ofkaq6NlkzVuyE=
-github.com/sagernet/sing-shadowsocks2 v0.2.0 h1:wpZNs6wKnR7mh1wV9OHwOyUr21VkS3wKFHi+8XwgADg=
-github.com/sagernet/sing-shadowsocks2 v0.2.0/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
-github.com/sagernet/sing-shadowtls v0.2.0 h1:cLKe4OAOFwuhmAIuPLj//CIL7Q9js+pIDardhJ+/osk=
-github.com/sagernet/sing-shadowtls v0.2.0/go.mod h1:agU+Fw5X+xnWVyRHyFthoZCX3MfWKCFPm4JUf+1oaxo=
-github.com/sagernet/sing-tun v0.6.1 h1:4l0+gnEKcGjlWfUVTD+W0BRApqIny/lU2ZliurE+VMo=
-github.com/sagernet/sing-tun v0.6.1/go.mod h1:fisFCbC4Vfb6HqQNcwPJi2CDK2bf0Xapyz3j3t4cnHE=
-github.com/sagernet/sing-vmess v0.2.0 h1:pCMGUXN2k7RpikQV65/rtXtDHzb190foTfF9IGTMZrI=
-github.com/sagernet/sing-vmess v0.2.0/go.mod h1:jDAZ0A0St1zVRkyvhAPRySOFfhC+4SQtO5VYyeFotgA=
-github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
-github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
-github.com/sagernet/utls v1.6.7 h1:Ep3+aJ8FUGGta+II2IEVNUc3EDhaRCZINWkj/LloIA8=
-github.com/sagernet/utls v1.6.7/go.mod h1:Uua1TKO/FFuAhLr9rkaVnnrTmmiItzDjv1BUb2+ERwM=
-github.com/sagernet/wireguard-go v0.0.1-beta.5 h1:aBEsxJUMEONwOZqKPIkuAcv4zJV5p6XlzEN04CF0FXc=
-github.com/sagernet/wireguard-go v0.0.1-beta.5/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
+github.com/sagernet/quic-go v0.52.0-sing-box-mod.3 h1:ySqffGm82rPqI1TUPqmtHIYd12pfEGScygnOxjTL56w=
+github.com/sagernet/quic-go v0.52.0-sing-box-mod.3/go.mod h1:OV+V5kEBb8kJS7k29MzDu6oj9GyMc7HA07sE1tedxz4=
+github.com/sagernet/sing v0.7.18 h1:iZHkaru1/MoHugx3G+9S3WG4owMewKO/KvieE2Pzk4E=
+github.com/sagernet/sing v0.7.18/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
+github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
+github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
+github.com/sagernet/sing-quic v0.5.2 h1:I3vlfRImhr0uLwRS3b3ib70RMG9FcXtOKKUDz3eKRWc=
+github.com/sagernet/sing-quic v0.5.2/go.mod h1:evP1e++ZG8TJHVV5HudXV4vWeYzGfCdF4HwSJZcdqkI=
+github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
+github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
+github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
+github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
+github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
+github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
+github.com/sagernet/sing-tun v0.7.10 h1:lLBaS9uL0mK/FCGDe3N4oKQxjMGfmv3u2/6jKtmq4Pw=
+github.com/sagernet/sing-tun v0.7.10/go.mod h1:pUEjh9YHQ2gJT6Lk0TYDklh3WJy7lz+848vleGM3JPM=
+github.com/sagernet/sing-vmess v0.2.7 h1:2ee+9kO0xW5P4mfe6TYVWf9VtY8k1JhNysBqsiYj0sk=
+github.com/sagernet/sing-vmess v0.2.7/go.mod h1:5aYoOtYksAyS0NXDm0qKeTYW1yoE1bJVcv+XLcVoyJs=
+github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
+github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
+github.com/sagernet/wireguard-go v0.0.1-beta.7 h1:ltgBwYHfr+9Wz1eG59NiWnHrYEkDKHG7otNZvu85DXI=
+github.com/sagernet/wireguard-go v0.0.1-beta.7/go.mod h1:jGXij2Gn2wbrWuYNUmmNhf1dwcZtvyAvQoe8Xd8MbUo=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=
-github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
-github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
-github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
+github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
+github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
+github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
-github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
-github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
+github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
+github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
+go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
+go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
+go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
+go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
+go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
-golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
+golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
+golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 h1:y5zboxd6LQAqYIhHnB48p0ByQ/GnQx2BE33L8BOHQkI=
+golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ=
golang.org/x/mobile v0.0.0-20231108233038-35478a0c49da h1:gS9sVMAeHM+gVBmM9bTM6vUi/NHv58O3QzJ3vjjN84M=
golang.org/x/mobile v0.0.0-20231108233038-35478a0c49da/go.mod h1:IEceR0jfVklLJXrbUe90rfdAFAYDW0SQwKl4qvO1GBQ=
-golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
-golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
-golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
-golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
+golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
+golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
-golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
-golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
-golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
+golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
+golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
-google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
-google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
+google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
diff --git a/libcore/http.go b/libcore/http.go
index db15a9af7..c4a8db500 100644
--- a/libcore/http.go
+++ b/libcore/http.go
@@ -10,24 +10,34 @@ import (
"errors"
"fmt"
"io"
+ "libcore/device"
+ "libcore/ech"
+ "log"
"net"
"net/http"
"net/url"
"os"
"strconv"
"sync"
+ "sync/atomic"
+ "time"
+ "github.com/sagernet/quic-go"
+ "github.com/sagernet/quic-go/http3"
"github.com/sagernet/sing/common/metadata"
"github.com/sagernet/sing/protocol/socks"
"github.com/sagernet/sing/protocol/socks/socks5"
)
+var errFailConnectSocks5 = errors.New("fail connect socks5")
+
type HTTPClient interface {
RestrictedTLS()
ModernTLS()
PinnedTLS12()
PinnedSHA256(sumHex string)
TrySocks5(port int32)
+ TryH3Direct()
KeepAlive()
NewRequest() HTTPRequest
Close()
@@ -58,16 +68,18 @@ var (
)
type httpClient struct {
- tls tls.Config
- client http.Client
- transport http.Transport
+ tls tls.Config
+ h1h2Transport http.Transport
+ h1h2Client http.Client
+ trySocks5 bool
+ tryH3Direct bool
}
func NewHttpClient() HTTPClient {
client := new(httpClient)
- client.client.Transport = &client.transport
- client.transport.TLSClientConfig = &client.tls
- client.transport.DisableKeepAlives = true
+ client.h1h2Client.Transport = &client.h1h2Transport
+ client.h1h2Transport.TLSClientConfig = &client.tls
+ client.h1h2Transport.DisableKeepAlives = true
return client
}
@@ -104,25 +116,36 @@ func (c *httpClient) PinnedSHA256(sumHex string) {
func (c *httpClient) TrySocks5(port int32) {
dialer := new(net.Dialer)
- c.transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
+ c.h1h2Transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
for {
socksConn, err := dialer.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(int(port)))
if err != nil {
+ if c.tryH3Direct {
+ return nil, errFailConnectSocks5
+ }
break
}
_, err = socks.ClientHandshake5(socksConn, socks5.CommandConnect, metadata.ParseSocksaddr(addr), "", "")
if err != nil {
+ if c.tryH3Direct {
+ return nil, errFailConnectSocks5
+ }
break
}
return socksConn, err
}
return dialer.DialContext(ctx, network, addr)
}
+ c.trySocks5 = true
+}
+
+func (c *httpClient) TryH3Direct() {
+ c.tryH3Direct = true
}
func (c *httpClient) KeepAlive() {
- c.transport.ForceAttemptHTTP2 = true
- c.transport.DisableKeepAlives = false
+ c.h1h2Transport.ForceAttemptHTTP2 = true
+ c.h1h2Transport.DisableKeepAlives = false
}
func (c *httpClient) NewRequest() HTTPRequest {
@@ -135,7 +158,7 @@ func (c *httpClient) NewRequest() HTTPRequest {
}
func (c *httpClient) Close() {
- c.transport.CloseIdleConnections()
+ c.h1h2Transport.CloseIdleConnections()
}
type httpRequest struct {
@@ -184,8 +207,17 @@ func (r *httpRequest) SetContentString(content string) {
}
func (r *httpRequest) Execute() (HTTPResponse, error) {
- response, err := r.client.Do(&r.request)
+ defer device.DeferPanicToError("http execute", func(err error) { log.Println(err) })
+ // full direct
+ if r.tryH3Direct && !r.trySocks5 {
+ return r.doH3Direct()
+ }
+ response, err := r.h1h2Client.Do(&r.request)
if err != nil {
+ // trySocks5 && tryH3Direct
+ if r.tryH3Direct && errors.Is(err, errFailConnectSocks5) {
+ return r.doH3Direct()
+ }
return nil, err
}
httpResp := &httpResponse{Response: response}
@@ -195,6 +227,121 @@ func (r *httpRequest) Execute() (HTTPResponse, error) {
return httpResp, nil
}
+type requestFunc func() (response *http.Response, err error)
+
+func (r *httpRequest) doH3Direct() (HTTPResponse, error) {
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ successCh := make(chan *http.Response, 1)
+ var finalErr error
+ var failedCount atomic.Uint32
+ var successCount atomic.Uint32
+ var mu sync.Mutex
+
+ funcs := []requestFunc{
+ // Http(s) With Ech
+ func() (response *http.Response, err error) {
+ request := r.request.Clone(context.Background())
+ echClient := &http.Client{
+ Transport: &http.Transport{
+ DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
+ var d net.Dialer
+ c, err := d.DialContext(ctx, network, addr)
+ if err != nil {
+ return c, err
+ }
+ domain := addr
+ if host, _, _ := net.SplitHostPort(addr); host != "" {
+ domain = host
+ }
+ echTls := ech.NewECHClientConfig(domain, &r.tls, gLocalDNSTransport)
+ return echTls.Client(ctx, c)
+ },
+ DisableKeepAlives: true,
+ },
+ }
+ return echClient.Do(request)
+ },
+ // H3 HTTPS
+ func() (response *http.Response, err error) {
+ request := r.request.Clone(context.Background())
+ h3Client := &http.Client{
+ Transport: &http3.Transport{
+ TLSClientConfig: r.tls.Clone(),
+ QUICConfig: &quic.Config{
+ MaxIdleTimeout: time.Second,
+ },
+ },
+ }
+ return h3Client.Do(request)
+ },
+ }
+
+ if r.request.URL.Scheme == "http" {
+ funcs = funcs[:1]
+ }
+
+ for i, f := range funcs {
+ go func(f requestFunc) {
+ defer device.DeferPanicToError("http", func(err error) { log.Println(err) })
+ defer func() {
+ if successCount.Load() == 0 {
+ if failedCount.Add(1) >= uint32(len(funcs)) {
+ // 全部失败了
+ cancel()
+ }
+ }
+ }()
+
+ var t string
+ switch i {
+ case 0:
+ t = "http(s)"
+ case 1:
+ t = "h3"
+ }
+
+ // 执行HTTP请求
+ rsp, err := f()
+ if rsp == nil || err != nil {
+ mu.Lock()
+ finalErr = errors.Join(finalErr, fmt.Errorf("%s: %w", t, err))
+ mu.Unlock()
+ if rsp != nil && rsp.Body != nil {
+ rsp.Body.Close()
+ }
+ return
+ }
+
+ // 处理 HTTP 状态码
+ if rsp.StatusCode != http.StatusOK {
+ hr := &httpResponse{Response: rsp}
+ err = fmt.Errorf("%s: %s", t, hr.errorString())
+ mu.Lock()
+ finalErr = errors.Join(finalErr, err)
+ mu.Unlock()
+ return
+ }
+
+ select {
+ case successCh <- rsp:
+ // 第一个成功的请求,不要关闭 body
+ successCount.Add(1)
+ default:
+ rsp.Body.Close()
+ }
+ }(f)
+ }
+
+ select {
+ case result := <-successCh:
+ return &httpResponse{Response: result}, nil
+ case <-ctx.Done():
+ return nil, finalErr
+ }
+}
+
type httpResponse struct {
*http.Response
diff --git a/libcore/init.sh b/libcore/init.sh
index 86fbbaf14..50bf4cfbd 100755
--- a/libcore/init.sh
+++ b/libcore/init.sh
@@ -10,7 +10,9 @@ fi
# Install gomobile
if [ ! -f "$GOPATH/bin/gomobile-matsuri" ]; then
git clone https://github.com/MatsuriDayo/gomobile.git
- pushd gomobile/cmd
+ pushd gomobile
+ git checkout origin/master2
+ pushd cmd
pushd gomobile
go install -v
popd
@@ -23,4 +25,4 @@ if [ ! -f "$GOPATH/bin/gomobile-matsuri" ]; then
mv "$GOPATH/bin/gobind" "$GOPATH/bin/gobind-matsuri"
fi
-gomobile-matsuri init
+GOBIND=gobind-matsuri gomobile-matsuri init
diff --git a/libcore/interface_monitor.go b/libcore/interface_monitor.go
index f4ec20aca..0167666e7 100644
--- a/libcore/interface_monitor.go
+++ b/libcore/interface_monitor.go
@@ -1,8 +1,6 @@
package libcore
import (
- "net/netip"
-
tun "github.com/sagernet/sing-tun"
"github.com/sagernet/sing/common/control"
"github.com/sagernet/sing/common/x/list"
@@ -10,40 +8,38 @@ import (
// wtf
-type interfaceMonitor struct {
-}
+type interfaceMonitorStub struct{}
-func (i *interfaceMonitor) Start() error {
+func (s *interfaceMonitorStub) Start() error {
return nil
}
-func (i *interfaceMonitor) Close() error {
+func (s *interfaceMonitorStub) Close() error {
return nil
}
-func (i *interfaceMonitor) DefaultInterfaceName(destination netip.Addr) string {
- return ""
-}
-
-func (i *interfaceMonitor) DefaultInterfaceIndex(destination netip.Addr) int {
- return 0
-}
-
-func (i *interfaceMonitor) DefaultInterface() *control.Interface {
+func (s *interfaceMonitorStub) DefaultInterface() *control.Interface {
return nil
}
-func (i *interfaceMonitor) OverrideAndroidVPN() bool {
+func (s *interfaceMonitorStub) OverrideAndroidVPN() bool {
return false
}
-func (i *interfaceMonitor) AndroidVPNEnabled() bool {
+func (s *interfaceMonitorStub) AndroidVPNEnabled() bool {
return false
}
-func (i *interfaceMonitor) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] {
+func (s *interfaceMonitorStub) RegisterCallback(callback tun.DefaultInterfaceUpdateCallback) *list.Element[tun.DefaultInterfaceUpdateCallback] {
return nil
}
-func (i *interfaceMonitor) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
+func (s *interfaceMonitorStub) UnregisterCallback(element *list.Element[tun.DefaultInterfaceUpdateCallback]) {
+}
+
+func (s *interfaceMonitorStub) RegisterMyInterface(interfaceName string) {
+}
+
+func (s *interfaceMonitorStub) MyInterface() string {
+ return ""
}
diff --git a/libcore/nb4a.go b/libcore/nb4a.go
index 775bb5c14..cfa1e7824 100644
--- a/libcore/nb4a.go
+++ b/libcore/nb4a.go
@@ -5,7 +5,7 @@ import (
"libcore/device"
"os"
"path/filepath"
- "runtime"
+ "runtime/debug"
"strings"
_ "unsafe"
@@ -14,6 +14,7 @@ import (
"github.com/matsuridayo/libneko/neko_common"
"github.com/matsuridayo/libneko/neko_log"
"github.com/sagernet/sing-box/nekoutils"
+ "github.com/sagernet/sing-box/option"
"golang.org/x/sys/unix"
)
@@ -29,12 +30,12 @@ func NekoLogClear() {
}
func ForceGc() {
- go runtime.GC()
+ go debug.FreeOSMemory()
}
func InitCore(process, cachePath, internalAssets, externalAssets string,
maxLogSizeKb int32, logEnable bool,
- if1 NB4AInterface, if2 BoxPlatformInterface,
+ if1 NB4AInterface, if2 BoxPlatformInterface, if3 LocalDNSTransport,
) {
defer device.DeferPanicToError("InitCore", func(err error) { log.Println(err) })
isBgProcess = strings.HasSuffix(process, ":bg")
@@ -43,6 +44,7 @@ func InitCore(process, cachePath, internalAssets, externalAssets string,
intfNB4A = if1
intfBox = if2
useProcfs = intfBox.UseProcFS()
+ gLocalDNSTransport = newPlatformTransport(if3, "", option.LocalDNSServerOptions{})
// Working dir
tmp := filepath.Join(cachePath, "../no_backup")
@@ -51,6 +53,8 @@ func InitCore(process, cachePath, internalAssets, externalAssets string,
// sing-box fs
resourcePaths = append(resourcePaths, externalAssets)
+ externalAssetsPath = externalAssets
+ internalAssetsPath = internalAssets
// Set up log
if maxLogSizeKb < 50 {
@@ -68,9 +72,6 @@ func InitCore(process, cachePath, internalAssets, externalAssets string,
defer device.DeferPanicToError("InitCore-go", func(err error) { log.Println(err) })
device.GoDebug(process)
- externalAssetsPath = externalAssets
- internalAssetsPath = internalAssets
-
// certs
pem, err := os.ReadFile(externalAssetsPath + "ca.pem")
if err == nil {
diff --git a/libcore/platform_box.go b/libcore/platform_box.go
index 7e7378c30..316cbef58 100644
--- a/libcore/platform_box.go
+++ b/libcore/platform_box.go
@@ -85,7 +85,7 @@ func (w *boxPlatformInterfaceWrapper) UsePlatformDefaultInterfaceMonitor() bool
}
func (w *boxPlatformInterfaceWrapper) CreateDefaultInterfaceMonitor(l logger.Logger) tun.DefaultInterfaceMonitor {
- return &interfaceMonitor{}
+ return &interfaceMonitorStub{}
}
func (w *boxPlatformInterfaceWrapper) UsePlatformInterfaceGetter() bool {
@@ -104,6 +104,10 @@ func (w *boxPlatformInterfaceWrapper) SendNotification(notification *platform.No
return nil
}
+func (s *boxPlatformInterfaceWrapper) SystemCertificates() []string {
+ return nil
+}
+
// Android not using
func (w *boxPlatformInterfaceWrapper) UnderNetworkExtension() bool {
diff --git a/nb4a.properties b/nb4a.properties
index d6dd91298..b2433289a 100644
--- a/nb4a.properties
+++ b/nb4a.properties
@@ -1,3 +1,4 @@
PACKAGE_NAME=moe.nb4a
-VERSION_NAME=1.3.9
-VERSION_CODE=43
+VERSION_NAME=1.4.2
+PRE_VERSION_NAME=pre-1.4.2-20260202-1
+VERSION_CODE=46