3

I am trying to create an iOS client that sends data to a server on a UDP socket over the device's cellular communication.

Following Does IOS support simultaneous wifi and 3g/4g connections? link to iOS Multipath BSD Sockets Test, I've tried implementing the solution in Swift 3, that is enumerate network interfaces in the device, identifying the Cellular interface (as suggested in Swift - Get device's IP Address), create a UDP socket and bind it to the sockaddr retrieved from the interface.

Implementation of socket programming in Swift was done by following examples from Socket Programming in Swift: Part 1 - getaddrinfo and following posts.

Unfortunately I received Operation not permitted when trying to send data on the socket, so instead I've tried creating the socket and binding it to the data from getaddrinfo called on a designated port (5555).

That too didn't do the trick. The interesting thing is that while trying to understand what's wrong, I created a test application for both methods, and when tested for 1000 consecutive create->bind->send->close, about 3-5 of the attempts actually did send the data without the error on either method.

Needless to say this was tested on an actual iPhone.

Quite at a loss, I'd appreciate any advice regarding this.

Code implemented in a static "SocketManager" class (edit: fixed sockaddr allocation size)

// Return IP address String, port String & sockaddr of WWAN interface (pdp_ip0), or `nil`
public static func getInterface() -> (String?, String?, UnsafeMutablePointer<sockaddr>?) {
    var host : String?
    var service : String?

    // Get list of all interfaces on the local machine:
    var ifaddr : UnsafeMutablePointer<ifaddrs>?
    var clt : UnsafeMutablePointer<sockaddr>?

    guard getifaddrs(&ifaddr) == 0 else {
        return (nil, nil, clt)
    }
    guard let firstAddr = ifaddr else {
        return (nil, nil, clt)
    }

    // For each interface ...
    for ifptr in sequence(first: firstAddr, next: { $0.pointee.ifa_next }) {
        let interface = ifptr.pointee
        let flags = Int32(ifptr.pointee.ifa_flags)

        /// Check for running IPv4 interfaces. Skip the loopback interface.
        if (flags & (IFF_UP|IFF_RUNNING|IFF_LOOPBACK)) == (IFF_UP|IFF_RUNNING) {
            let addrFamily = interface.ifa_addr.pointee.sa_family
            if addrFamily == UInt8(AF_INET) { //Interested in IPv4 for in particular case

                // Check interface name:
                let name = String(cString: interface.ifa_name)
                print("interface name: \(name)")
                if  name.hasPrefix("pdp_ip") { //cellular interface

                    // Convert interface address to a human readable string:
                    let ifa_addr_Value = interface.ifa_addr.pointee
                    clt = UnsafeMutablePointer<sockaddr>.allocate(capacity: 1)
                    clt?.initialize(to: ifa_addr_Value, count: 1)

                    var hostnameBuffer = [CChar](repeating: 0, count: Int(NI_MAXHOST))
                    var serviceBuffer = [CChar](repeating: 0, count: Int(NI_MAXSERV))
                    getnameinfo(interface.ifa_addr, socklen_t(ifa_addr_Value.sa_len),
                            &hostnameBuffer, socklen_t(hostnameBuffer.count),
                            &serviceBuffer,
                            socklen_t(serviceBuffer.count),
                            NI_NUMERICHOST | NI_NUMERICSERV)
                    host = String(cString: hostnameBuffer)
                    if let host = host {
                      print("found host \(String(describing: host))")
                    }

                    service = String(cString: serviceBuffer)
                    if let service = service {
                        print("found service \(String(describing: service))")
                    }
                    break;
                }
            }
        }
    }
    freeifaddrs(ifaddr)

    return (host, service, clt)
}

public static func bindSocket(ip: String, port : String, clt : UnsafeMutablePointer<sockaddr>, useCltAddr : Bool = false) -> Int32 {

    print("binding socket for IP: \(ip):\(port) withCltAddr=\(useCltAddr)")
    var hints = addrinfo(ai_flags: 0,
                            ai_family: AF_INET,
                            ai_socktype: SOCK_DGRAM,
                            ai_protocol: IPPROTO_UDP,
                            ai_addrlen: 0,
                            ai_canonname: nil,
                            ai_addr: nil,
                            ai_next: nil)

    var connectionInfo : UnsafeMutablePointer<addrinfo>? = nil

    let status = getaddrinfo(
            ip,
            port,
            &hints,
            &connectionInfo)
    if status != 0 {
        var strError: String
        if status == EAI_SYSTEM {
            strError = String(validatingUTF8: strerror(errno)) ?? "Unknown error code"
        } else {
            strError = String(validatingUTF8: gai_strerror(status)) ?? "Unknown error code"
        }
        print(strError)
        return -1
    }

    let socketDescriptor = useCltAddr ? socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) : socket(connectionInfo!.pointee.ai_family, connectionInfo!.pointee.ai_socktype, connectionInfo!.pointee.ai_protocol)

    if socketDescriptor == -1 {
        let strError = String(utf8String: strerror(errno)) ?? "Unknown error code"
        let message = "Socket creation error \(errno) (\(strError))"
        freeaddrinfo(connectionInfo)
        print(message)
        return -1
    }
    let res = useCltAddr ? bind(socketDescriptor, clt, socklen_t(clt.pointee.sa_len)) : bind(socketDescriptor, connectionInfo?.pointee.ai_addr, socklen_t((connectionInfo?.pointee.ai_addrlen)!))

    if res != 0 {
        let strError = String(utf8String: strerror(errno)) ?? "Unknown error code"
        let message = "Socket bind error \(errno) (\(strError))"
        freeaddrinfo(connectionInfo)
        close(socketDescriptor)
        print(message)
        return -1
    }
    freeaddrinfo(connectionInfo)
    print("returned socket descriptor \(socketDescriptor)")
    return socketDescriptor   
}

//returns 0 for failure, 1 for success
public static func sendData(toIP: String, onPort : String, withSocketDescriptor : Int32, data : Data) -> Int{

    print("sendData called for targetIP: \(toIP):\(onPort) with socket descriptor: \(withSocketDescriptor)")
    var target = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: MemoryLayout<sockaddr_in>.size)

    target.pointee.sin_family = sa_family_t(AF_INET)
    target.pointee.sin_addr.s_addr = inet_addr(toIP)
    target.pointee.sin_port = in_port_t(onPort)!

    var res = 0
    data.withUnsafeBytes { (u8Ptr: UnsafePointer<UInt8>) in
        let rawPtr = UnsafeRawPointer(u8Ptr)
        withUnsafeMutablePointer(to: &target) {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                let bytesSent = sendto(withSocketDescriptor, rawPtr, data.count, 0, $0, socklen_t(MemoryLayout.size(ofValue: target)))
                if bytesSent > 0 {
                    print(" Sent \(bytesSent) bytes ")
                    res = 1
                }
                if bytesSent == -1 {
                    let strError = String(utf8String: strerror(errno)) ?? "Unknown error code"
                    let message = "Socket sendto error \(errno) (\(strError))"
                    print(message)
                }
            }
        }
    }
    return res
}

public static func closeSocket(socketDescriptor : Int32, clt : UnsafeMutablePointer<sockaddr>) {
    print("closing socket descriptor \(socketDescriptor)")
    close(socketDescriptor)
    clt.deinitialize()
    clt.deallocate(capacity: 1) 
}

On ViewController:

override func viewDidLoad() {
    super.viewDidLoad()
    var i = 0
    for _ in 0..<1000 {
        i += connectSendClose(withDescriptor: false) // change withDescriptor to switch socket create/bind method
    }
    print("Sent \(i) packets")   
}

private func connectSendClose(withDescriptor : Bool) -> Int {
    let interface = SocketManager.getInterface()
    guard let ip = interface.0 else {
        print("no relevant interface")
        return 0
    }
    guard let clt = interface.2 else {
        print("no addr")
        return 0
    }
    let socketDescriptor = SocketManager.bindSocket(ip: ip, port: "5555", clt: clt, useCltAddr: withDescriptor)
    if socketDescriptor == -1 {
        print("faild to configure socket")
        return 0
    }
    let serverIP = "59.122.442.9" //dummy IP, test was preformed on actual server 
    let serverPort = "10025" //dummy port, test was preformed on actual server
    let input = 42.13
    var value = input
    let data = withUnsafePointer(to: &value) {
        Data(bytes: UnsafePointer($0), count: MemoryLayout.size(ofValue: input))
    }

    let res = SocketManager.sendData(toIP: serverIP, onPort: serverPort, withSocketDescriptor: socketDescriptor, data: data)

    SocketManager.closeSocket(socketDescriptor: socketDescriptor, clt: clt)
    return res

}
Undo
  • 25,519
  • 37
  • 106
  • 129
Cha.OS
  • 88
  • 10
  • That @brianhaak guy has several posts claiming you can connect to mobile data even if wifi is on. But i was unable to confirm this myself through any documentation. And the idea itself is counter intuitive. If im an iphone user, and i have wifi on, i expect every app to use the wifi connection. I would consider apps sneakily using the mobile data to be a security issue. – Sal Aug 06 '17 at 13:10
  • @Paul this particular app will not use mobile data sneakily - users will be well informed as to data usage upfront. As for brianhaak's posts, he's referring to using native C code. Though that should work (iOS being a Unix-Like OS), I'm mostly interested in the Swift implementation at this time: As far as I understand it, these particular Swift networking functions are wrappers for the native code, thus I'm puzzled as to the errors I'm receiving and curious to know if there's an implementation error on my end, or something else at work. – Cha.OS Aug 06 '17 at 13:36
  • I do not have the setup to test your code currently, but note that in your code to "duplicate" the socket address to allocated memory, the `capacity/count` parameters denote the number of *items,* not the number of bytes. – Martin R Aug 06 '17 at 13:48
  • @MartinR I am aware my code currently doesn't support IPv6, this was done by design due to the fact my testing device currently only outputs the interface in IPv4 (IPv6 support will be added once the actual problem will be solved). As for `capacity/count`, I assume you're referring to `initialize`? Thanks, I'll fix that – Cha.OS Aug 06 '17 at 13:56
  • `clt = UnsafeMutablePointer.allocate(capacity: Int(ifa_addr_Value.sa_len))` allocates memory for `ifa_addr_Value.sa_len` items of type `sockaddr`, i.e. `ifa_addr_Value.sa_len * sizeof(sockaddr)` bytes. – Martin R Aug 06 '17 at 13:58
  • @MartinR Thanks, I fixed it to be `clt = UnsafeMutablePointer.allocate(capacity: 1) clt?.initialize(to: ifa_addr_Value, count: 1)` – Cha.OS Aug 06 '17 at 14:15

1 Answers1

1

Edit: Fixed Network byte order bug in creation of target sockadd_in.

Alright, found the problem: First, as Martin noted, I miss used UnsafeMutablePointer allocation as I took capacity/count parameters as bytes.

This was done also when I allocated sockaddr_in for server details in sendData function (var target = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: MemoryLayout<sockaddr_in>.size as opposed to var target = UnsafeMutablePointer<sockaddr_in>.allocate(capacity: 1).

After fixing this to, I started to get better results (about 16 out of 1000 sends passed), but obviously it was not enough. I Found Send a message using UDP in Swift 3, and decided to change the use of sockaddr_in to var target = sockaddr_in(sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size), sin_family: sa_family_t(AF_INET), sin_port: in_port_t(onPort)!, sin_addr: in_addr(s_addr: inet_addr(toIP)), sin_zero: (0,0,0,0, 0,0,0,0)), everything works.

I'm still puzzled as to why using Unsafe Memory with this struct didn't work though.

Another thing: I moved this code back to my actual App, trying to bind the socket to my own addrinfo via getaddrinfo constantly fails with Can't assign requested address, using the one I get from enumerated interfaces works, but I receive lots of No buffer space available errors (something for another research :).

In the test code, both binding methods (enumerated & getaddrinfo) work fine.

Fixed sendData function:

public static func sendData(toIP: String, onPort : String, withSocketDescriptor : Int32, data : Data) -> Int{

    print("sendData called for targetIP: \(toIP):\(onPort) with socket descriptor: \(withSocketDescriptor)")        
    var target = sockaddr_in(sin_len: __uint8_t(MemoryLayout<sockaddr_in>.size), sin_family: sa_family_t(AF_INET), sin_port: in_port_t(bigEndian: onPort)!, sin_addr: in_addr(s_addr: inet_addr(toIP)), sin_zero: (0,0,0,0, 0,0,0,0))

    var res = 0
    data.withUnsafeBytes { (u8Ptr: UnsafePointer<UInt8>) in
        let rawPtr = UnsafeRawPointer(u8Ptr)
        withUnsafeMutablePointer(to: &target) {
            $0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
                let bytesSent = sendto(withSocketDescriptor, rawPtr, data.count, 0, $0, socklen_t(MemoryLayout.size(ofValue: target)))
                if bytesSent > 0 {
                    print(" Sent \(bytesSent) bytes ")
                    res = 1
                }
                if bytesSent == -1 {
                    let strError = String(utf8String: strerror(errno)) ?? "Unknown error code"
                    let message = "Socket sendto error \(errno) (\(strError))"
                    print(message)
                }
            }
        }
    }
    return res        
}
Cha.OS
  • 88
  • 10
  • Hey @Cha.OS did you ever figure out how to resolve the "Can't assign requested address" issue? I'm seeing that every time while trying to send a message to a server. – Eric Oct 20 '17 at 23:13
  • Hi @Eric, unfortunately not, as my chosen solution is based on binding the socket to the enumerated interface, where this error isn't occurring. I would suggest checking all parameters are send with Network Byte Order correctly, I had a few places where not enforcing this caused for some of the networking errors I had (see latest edit in my answer for example). – Cha.OS Oct 22 '17 at 08:08
  • Hey @Cha.OS, thanks for the response. What do you mean by enumerated interface? I don't see mention of that in your original post so wondering how you are binding with a different interface from the one you're getting from `getifaddrs`? – Eric Oct 23 '17 at 17:16
  • @Eric, I'll try to clarify: I'm binding my socket to the same interface retrieved with `getifaddrs` in two different ways, either by using `sockaddr` directly (dubbed `clt`) or by manually creating `sockaddr` struct for the IP & port via `getaddrinfo` (see `bindSocket` in my original post and follow the different flows depending on the value of `useCltAddr`). The particular error you asked about occurred only when I was trying to bind the socket to the manually created `addrinfo`, but it doesn't occur when I'm binding the socket to the `sockaddr` struct I retrieve with `getifaddrs`. – Cha.OS Oct 24 '17 at 09:02