Skip to content

rework: fix incorrect tunnel configuration causing confusion + switch to explicit CIDR format + remove swapping logic restricted to fakeIP - #23

Merged
se2crid merged 8 commits into
jkcoxson:mainfrom
mahee96:main
Aug 17, 2026
Merged

Conversation

@mahee96

@mahee96 mahee96 commented Aug 16, 2026

Copy link
Copy Markdown
  1. The tunnel was incorrectly configured where source and dest were same tunnel iface ip.

  2. swapping was strictly restricted to fake IP addr only but we used fakeIP as 10.7.0.1/24, which when AND'd with subnet /24 gives 10.7.0.0/24 so when we restricted the swapping to just 10.7.0.1 it would never work basically. But wireguard & EMProxy used to work coz EMProxy didn't restrict itself to specific IPs and let tunnel config be authoritative. so we needed to remove the restriction too to make anything meaningful out of this tunnel for other local development real use cases and even for sidestore's auto-discovery or any kind of auto-discovery that checks routing table in kernel, to work.

  3. our fake ip which when AND with subnet /24 will give 10.7.0.0/24 meaning kernel was storing the base address but then the deviceIP too was 10.7.0.0/24 causing severe confusion in client/app side as if kernel was reporting incorrect ips.
    But it was the tunnel config ips we chose that were bad. so updated default tunnel configs as TunnelIP(ifaceIP) = 10.7.0.0/24 and DeviceIP(PeerIP) = 11.7.0.0/24

  4. Implemented CIDR validator which performs validation of inputs and flags incorrect configuration properly.

  5. Due to same, switched to full CIDR based IP addressing format coz subnet mask allows invalids like 255.255.255.251 which are preventable using CIDR prefixes /32, /24 etc that restricts to 2^x subnet masks only.

  6. added some validations etc in UI so that it flags and restricts from accepting invalid CIDR IP configs.

  7. added "allow intermediate addresses" flag which is opt-in to allow non canonical ips such as 10.7.0.1/24 or 11.3.0.2/30 which would usually resolve (when AND'd with mask) to 10.7.0.0/24 [range (2^32 - 2^24 = 2^8 ie 256 addrs) = subnet 10.7.0.0 <-> 10.7.0.255] and 11.3.0.0/24 [range (2^32 - 2^30 = 2^2 ie 4 addrs) = subnet 11.3.0.0 <-> 11.3.0.3]. ie we now show warning if user explicitly enabled non canonical ip subnets and if "allow intermediate addresses" was ON, otherwise if OFF, we just show as ERROR ie not allowed.

  8. again most of the validations are for destination mostly coz multiple IPs assigned to iface isn't much of a problem coz kernel can decide which ip on the iface to stamp as source IP for outbound traffic, so the main concern is always destination Routes of the tunnel.

  9. a p2p destination IP that has subnet /32 is best choice to connect to target ip directly, but we do allow subnet addrs.

  10. Decoupled iface subnetMask from destinationRoute IP's subnetmask, coz hey we can have 1 iface IP and multiple destination routes with different subnets! and CIDR already helps with it anway now since we embed subnet mask into ip itself using CIDR prefix notation.

I think that is all that was covered by this fix.

for future readers, the following is simple config of a VPN routing (which is already in this PR)

        // tunnel iface configuration
        let ifaceIPv4 = NEIPv4Settings(addresses: [ifaceEndpoint.ip], subnetMasks: [ifaceEndpoint.subnetMask])
        let tunnelDestinationIPv4Routes = [
            // actual destination routes of this VPN tunnel
            NEIPv4Route(destinationAddress: peerEndpoint.ip, subnetMask: peerEndpoint.subnetMask)
        ]
        ifaceIPv4.includedRoutes = tunnelDestinationIPv4Routes
        ifaceIPv4.excludedRoutes = [.default()]

        // Tunneling config
        let settings = NEPacketTunnelNetworkSettings(
            // NOTE: 'tunnelRemoteAddress' is just for UI concerns and is not involved in routing
            tunnelRemoteAddress: peerEndpoint.ip
        )   
        settings.ipv4Settings = ifaceIPv4
        
        tunnelLog("Calling setTunnelNetworkSettings...")
        setTunnelNetworkSettings(settings) { error in
            if let error = error {
                tunnelLog("Failed to set settings: \(error.localizedDescription)")
                return completionHandler(error)
            }
            tunnelLog("Tunnel network settings set successfully. Starting packet loops.")
            self.setPackets()
            completionHandler(nil)
        }

… to explicit CIDR format coz subnet mask allows invalids like 255.255.255.251 which are preventable using CIDR prefixes /32, /24 etc.

@stossy11 stossy11 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! Thank you mahee.

@CelloSerenity
CelloSerenity requested a review from se2crid August 16, 2026 18:11
… and info.plist properly so one can reconfigure without touching pbxproj again for codesigning or info.plist updates.
@mahee96

mahee96 commented Aug 17, 2026

Copy link
Copy Markdown
Author

some background on things:

NW basics:

  1. on a device, a network interface can support multiple IPs to be assocaited with.
  2. usually 0.0.0.0 (default IP), x.x.x.0 (network base), 255.255.255.255 (broadcast), x.x.x.1 (gateway) etc are reserved IPs on a physical network interface like NIC, wifi, ether etc which work by sending ARP broadcasts to resolve the local network.
  3. now tunnel is special case since it is virtual and doesn't need a specific reserved set of IPs like network base addr, broadcast addr etc. so full 0-255 (256 ips are available to use).
  4. now since a interface can support multiple ips to be associated with one can have a subnet range assigned to it too ex: (iphone-wifi-ip: 192.168.10.2/32) or mac-ethernet-ip: 192.168.10.0/30 ie a range of addresses from 192.168.10.0 till 192.168.10.3 coz 2^32 - 2^30 = 2^2 => 4 ips)
  5. kernel which manages the on device routing table, needs to know who is source when sending outbound traffic and since interface can support multiple IPs it can become ambiguous to chose if multiple subnets are used. ex (wifi ifaceIPs can be single 192.168.10.0/24 or even multiple 192.168.10.0/24, 10.7.0.0/24 [seems this is modern capability coz initially a single Iface used to be just 1 IpAddr])
  6. kernel needs the routes to be clear, ie if a physical interface like wifi, ethernet was used, its IP cannot be x.x.x.0, 255.255.255.255, 0.0.0.0 and will reject if someone tried to assign these to an interface. but for virtual tunnels they are free to be assigned coz there is no real hw that is doing ARP broadcasts etc.

Tunnel Basics:
7. In terms of tunnel since it is also a interface type but virtual it can also support multiple IPs as mentioned in 1.
8. that is why NEIPv4Settings allows range of addrs, for each IP there needs to be corresponding subnet ie /32 (255.255.255.255) if single endpoint or /24 (255.255.255.0) etc for range

let ifaceIPv4 = NEIPv4Settings(addresses: [ifaceEndpoint.ip], subnetMasks: [ifaceEndpoint.subnetMask])
  1. now a tunnel gotta need routes/destinations, for that we can again specify many IPs (not sure about the limit on xnu kernel).
let tunnelDestinationIPv4Routes = [
    // actual destination routes of this VPN tunnel
    NEIPv4Route(destinationAddress: peerEndpoint.ip, subnetMask: peerEndpoint.subnetMask)
]
  1. a route means again it could be a simple single endpoint ex: x.x.x.x/32 or x.x.x.x/28 (ie a range). In 9 I have 1 ip but of subnet range
  2. we can have many such routes. this means one can specify single subnet that includes 4 ips by NEIPv4Route(10.7.0.0/30) (means 10.7.0.0 <-> 10.7.0.3)
    11a. or a range of routes explicitly by 4 NEIPv4Route ie [NEIPv4Route(10.7.0.0/32),NEIPv4Route(10.7.0.1/32),NEIPv4Route(10.7.0.2/32),NEIPv4Route(10.7.0.3/32)]
  3. the advantage of 11a vs 11 is that Kernel knows app point2point routes in 11a, so routing table can contain explicit routes, but for subnet range in 11 it cannot infer all posible devices that can exist beforehand so it just holds 1 route to entire subnet.
  4. the 8, 9 are the crucial keys in apple's NEPacketTunnelProvider API.

How it connects to SideStore:
14. well we used 10.7.0.0/24 for the Tunnel's own Interface IP, meaning we used a subnet range when we simply didn't have to.
14a. coz source IP subnetting is only required when devices on the other end wants to talk to multiple devices (within same subney) on our end thru the tunnel.
14b. ex: if the tunnel was inside a router which managed multiple devices, then having a subnet range makes sense for the interface coz all such devices are behind the tunnel where tunnel/router served as gateway to external network.
15. Now on top of that, we used single route and the route had 10.7.0.1/24 meaning upto 32-24 = 8 so 2^8 = 256 device range in this subnet.
15a. but if we started at 10.7.0.1 and subnet range is 256 ips, 10.7.0.1 + 0.0.0.255 = 10.7.0.256 which is outside valid IPs range.
15b. so kernel by default uses the subnet MASK of /24 which is 255.255.255.0 to do AND operation on IP means we get (10.7.0.1 & 255.255.255.0) => 10.7.0.0
15c. basically even though we ask the device IP to be 10.7.0.1, due to /24 subnet maks, the range starts actually from 10.7.0.0 till 10.7.0.255.
16. can existing clients/apps who used hardcoded still connect to 10.7.0.1? ofcourse they can still connect, coz 10.7.0.1 is still within 10.7.0.0 and 10.7.0.255.
17. is this why existing setup worked? Yes it worked due to 16.
18. so what is the issue now? The issue was that since we used interface IP as 10.7.0.0/24 and route/device/fake ip as 10.7.0.1/24 (which resolves to 10.7.0.0 as start addr) kernel sets up route as from 10.7.0.0 -> 10.7.0.0 (and not 10.7.0.1).
19. Now, what we have changed in recent PR? we are planning to keep interface IP and route/device/fake ip ranges completely independent so kernel knows what is what clearly.
20. this means iface Ip takes a different subnet like 11.7.0.0/24 or anything that doesn't share the prefix to route/device/fake ip ie 10.7.0

iface => 11.7.0.0/24
dest  => 10.7.0.0/24

result: both dont share the first 3 octets anymore coz 11.7.0 != 10.7.0
  1. we can even use 10.7.1.0/24 for the tunnel/interface IP, coz then it becomes
iface => 10.7.1.0/24
dest  => 10.7.0.0/24

result: both dont share the first 3 octets anymore coz 10.7.1 != 10.7.0
  1. we can refine futher ex:
iface => 10.7.1.1/32      (using /32 coz well we are the only device behind this tunnel on source side)
dest  => 10.7.0.1/24      (the dest side of tunnel can still be a range)

result: still both dont share the first 3 octets anymore coz 10.7.1 != 10.7.0
  1. or if we gonna go full point2point (or peer2peer) ex:
iface => 10.7.1.1/32      (single source)
dest  => 10.7.0.1/32      (single destination)

result: still both dont share the first 3 octets anymore coz 10.7.1 != 10.7.0

@mahee96

mahee96 commented Aug 17, 2026

Copy link
Copy Markdown
Author

discussed with team and seems /32 is fine ie point to point and we wanted to still keep to reserved private range 10.x.x.x.

so choosing 23, ie:

iface => 10.7.1.1/32      (single source)
dest  => 10.7.0.1/32      (single destination)

just for reference we used to have wireguard config in SideStore.conf as below which was /32 target anyways.

[Interface]
PrivateKey = AIIeeUDvk3NeAFJ9BWCQvPJize/9WZibMnGJ/0rt5k4=
Address = 10.7.0.10/24

[Peer]
PublicKey = kHDoekeYhBvfW9a9UQ+UCmpbG423eejTjcjW+DT+JF0=
AllowedIPs = 10.7.0.1/32
Endpoint = 127.0.0.1:51820
PersistentKeepalive = 25

@se2crid
se2crid merged commit b31b06c into jkcoxson:main Aug 17, 2026
@se2crid

se2crid commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants