2013年3月17日星期日

iPhone 反向 USB 网络共享 - Linux 篇

所谓“反向共享”,就是电脑连接上网络,iPhone 没有,然后把网络连接共享给 iPhone。
网上有一些方案是利用代理服务器的,不完美,这里利用的是 OpenVPN,可以说是 90% 完美了。
为了避免伸手党(其实是我自己懒得写),我就不说详细了,把大概的内容说一下。如果谁感兴趣可以写一份更详细的教程。

  1. 为什么需要反向网络共享?
    你可能在一个没有 Wi-Fi 接入的地方(或者你的机器是行货 3GS 无 Wi-Fi),然后想向 iPhone 里下载一些东西,这个时候反向共享就有用了。
  2. iPhone 端安装软件
    首先,你的 iPhone 必须越狱(废话),然后从 Cydia 安装这些包:
    openssh GuizmOVPN
    如果你找不到 openssh 请把身份设置成“开发者”,后面那个是收费的,可以下载免费试用版,我们不需要主程序,只需要里面打过特殊补丁的 OpenVPN 客户端。
    2015年1月1日更新:请不要升级 GuizmOVPN 到 1.2.1!因为它将 TUNEMU 换成了 UTUN,因而不能工作!幸好我有备份:com.guizmo.openvpn_1.2.0_iphoneos-arm.deb
    至于怎么安装我就不说了,有 2G/3G/4G 的连上网下。没有的可以用 Cyder 2。截至发文,Cyder 2 仍然没有支持 iOS 5 以上系统,所以可以用 Cyder 下载 deb 包及其依赖,然后用 i-FunBox 拷贝进 iPhone 的 Cyder 自动安装目录。
    如果你打算用命令行 apt-get install,请用下面的软件包名:
    openssh com.guizmo.openvpn
  3. 电脑端安装软件
    你需要这些:
    openssh openvpn usbmuxd 你可能还想要 libimobiledevice ifuse
    根据发行版不同,你用的发行版可能是别的名字。
  4. 配置 OpenVPN
    怎么生成证书不在本文讨论的范畴(Google is your friend)。下面我们来讨论特殊的地方。
    假设你已经生成好 ca.crt ca.key dh1024.pem server.crt server.key client.crt client.key 了。我们来写 server.ovpn
    local 127.0.0.1
    port 1194
    proto tcp
    dev tun
    ca ca.crt
    cert server.crt
    key server.key
    dh dh1024.pem
    server 10.8.0.0 255.255.255.0
    push "dhcp-option DNS 8.8.8.8"
    push "dhcp-option DNS 8.8.4.4"
    client-to-client
    duplicate-cn
    keepalive 10 120
    verb 3
    然后是 client.ovpn
    client
    dev tun
    proto tcp
    remote localhost 1194
    nobind
    persist-key
    persist-tun
    ca ca.crt
    cert client.crt
    key client.key
    ns-cert-type server
    verb 3
    up ./guizmovpn_updown.sh
    down ./guizmovpn_updown.sh
    然后是关键的 guizmovpn_updown.sh,这个文件应该在 /Applications/GuizmOVPN.app 有。但是我的这个版本貌似可以用,如果你的版本不能用,试试看我的:
    #!/bin/bash
    
    #######################
    # GuizmOVPN_updown.sh #
    # Version 1.0.6       #
    #######################
    
    function CopyKey
    {
     if [ "`KeyExists $1`" == "NO" ] ; then
      return
     fi
            source=$1;
            dest=$2;
     error="0";
            echo "d.init" >/tmp/tmpscutil
            for param in `echo "show $1" | /usr/sbin/scutil | grep -v "<dictionary>" | tr -d '\n' | tr '}' '\n' | awk '{ print $1 }'` ; do
                    value=`echo "show $1" | /usr/sbin/scutil| grep -v "<dictionary>" | tr -d '\n' | tr '}' '\n' | grep $param`
      if $( echo $value | grep --quiet '<array>' ) ; then
                            unset array_val
                            for arraynum in 2 3 4 5 6 7 8 9 ; do
                                    val=`echo $value | cut -d '{' -f 2 | cut -d ':' -f $arraynum | cut -d ' ' -f 2`
                                    if [ "$val" != "" ] ; then
                                            array_val[$(($arraynum-1))]=$val;
                                    fi
                            done
                            if [ ${#array_val[@]} ]; then
                                    echo "d.add $param * ${array_val[*]}" >>/tmp/tmpscutil
                            else
        error="1" 
       fi
                    else
                            val=`echo "$value" | awk '{ print $3 }'`
                            if [ "`echo "$val" | tr -d ' '`"  == "" ] ; then
                                    error="1";
                            else
                                    echo "d.add $param $val" >>/tmp/tmpscutil
                            fi
                    fi
    
            done
            echo "set $dest" >>/tmp/tmpscutil
     if [ "$error"=="0" ] ; then
      cat /tmp/tmpscutil | /usr/sbin/scutil
     else
      echo "Error reading key $1"
     fi 
    }
    
    function GetActualDNS
    {
     OLDDNS=`echo "show State:/Network/Service/$1/DNS" | /usr/sbin/scutil | grep -A 1 "ServerAddresses : " | grep "0 : " | awk '{print $3}' | tr -d " "`
     echo $OLDDNS  
    }
    
    function KeyExists
    {
     if [ "`echo "show $1" | /usr/sbin/scutil | tr -d " "`" == "Nosuchkey" ] ; then
      echo "NO" ;
     else
      echo "YES" ;
     fi
    }
    
    PSID=$( (/usr/sbin/scutil | grep PrimaryService | sed -e 's/.*PrimaryService : //')<< EOF
    open
    get State:/Network/Global/IPv4
    d.show
    quit
    EOF
    )
    
    case "$script_type" in
       up)
         # Stop the APSd
         #/bin/launchctl unload /System/Library/LaunchDaemons/com.apple.apsd.plist
         
         # Remove informations about a previous connection
         echo "remove State:/Network/Service/OpenVPN/DNS" | /usr/sbin/scutil >/dev/null 2>&1
         echo "remove State:/Network/Service/OpenVPN/IPv4" | /usr/sbin/scutil >/dev/null 2>&1
         echo "remove State:/Network/Service/OpenVPN/PPP" | /usr/sbin/scutil >/dev/null 2>&1
         echo "remove State:/Network/Service/OpenVPN/PSID" | /usr/sbin/scutil >/dev/null 2>&1
     
         # Check if we need to handle DNSPush
         unset dns
         unset domain
         if [ "$DNSPush" == "Y" ] ; then
     # Store the PSID that we modify, in order to restore the correct one on disconnection
            if [ "$PSID" != "" ] ; then
                    echo "d.init" >/tmp/tmpscutil
                    echo "d.add PSID $PSID" >>/tmp/tmpscutil
                    echo "set State:/Network/Service/OpenVPN/PSID" >>/tmp/tmpscutil
                    cat /tmp/tmpscutil | /usr/sbin/scutil
            fi
    
     # Save the DNS settings
     # If it has already been set, don't save it again
     if [ "`KeyExists State:/Network/Global/DNSold`" == "NO" ] ; then 
      CopyKey Setup:/Network/Service/$PSID/DNS Setup:/Network/Service/$PSID/DNSold ; 
      CopyKey State:/Network/Service/$PSID/DNS State:/Network/Service/$PSID/DNSold ;
      CopyKey State:/Network/Global/DNS State:/Network/Global/DNSold ; 
     fi
    
          n=1; i=0; j=0;
          while o=foreign_option_${n}; o=${!o}; [ "$o" ]
          do
               case $o in
                'dhcp-option DNS '*)        dns[i++]=${o/dhcp-option DNS /};;
                  'dhcp-option DOMAIN '*)     domain[j++]=${o/dhcp-option DOMAIN /} ;;
               esac;
               let n++
          done
    
     # Check if we need to try to keep local DNS
          if [ "$DNSKeep" == "Y" ] ; then
      # If DNS push has been set, add the actual DNS in the list, in case the DNS pushed don't respond
      if [ $i != 0 ] && [ $i -le 3 ] ; then
              dns[i]=`GetActualDNS $PSID`;
      
        # Check if the DNS is different from the gateway IP.
        # If it's the case, add a route to it
        if [ "$traffic_is_redirected" == "1" ] && [ "$dns[i]" != "" ] && [ "$route_net_gateway" != "${dns[i]}" ] && [ "$route_net_gateway" != "" ] ; then
        echo "Add a route to ${dns[i]} with gateway $route_net_gateway" ;
        /sbin/route add -net ${dns[i]} $route_net_gateway 255.255.255.255;
        fi 
           fi
     fi
    
          if [ "${dns[0]}" != "" ] ; then
      echo "Setting DNS to /Network/Service/$PSID/DNS (${dns[*]})"
      echo "d.init" >/tmp/tmpscutil
             echo "d.add ServerAddresses * ${dns[*]}" >>/tmp/tmpscutil 
      if [ "${domain[0]}" != "" ] ; then
       echo "Setting domain to /Network/Service/$PSID/DNS (${domain[*]})" 
       echo "d.add SupplementalMatchDomains * ${domain[*]}" >>/tmp/tmpscutil
      fi
      echo "set State:/Network/Service/$PSID/DNS" >>/tmp/tmpscutil 
      
      cat /tmp/tmpscutil | /usr/sbin/scutil 
    
      # For a strange reason, some devices don't use the "State" but the "Setup", so we copy the key.
      if [ "`KeyExists Setup:/Network/Service/$PSID/DNS`" == "YES" ] ; then
                     CopyKey State:/Network/Service/$PSID/DNS Setup:/Network/Service/$PSID/DNS 
                    fi
      CopyKey State:/Network/Service/$PSID/DNS State:/Network/Service/OpenVPN/DNS 
                    CopyKey State:/Network/Service/$PSID/DNS State:/Network/Global/DNS 
     fi
         fi
         
         # Add informations about the connection
         # IPv4
         echo "d.init" >/tmp/tmpscutil
         echo "d.add DestAddresses * $trusted_ip" >>/tmp/tmpscutil
         echo "d.add ServerAddress $trusted_ip" >>/tmp/tmpscutil
         echo "d.add InterfaceName ppp0" >>/tmp/tmpscutil
         echo "d.add Addresses * $ifconfig_local" >>/tmp/tmpscutil
         echo "d.add SubnetMasks * 255.255.255.255" >>/tmp/tmpscutil
         echo "d.add NetworkSignature VPN.RemoteAddress=$trusted_ip" >>/tmp/tmpscutil
         echo "set Setup:/Network/Service/OpenVPN/IPv4" >>/tmp/tmpscutil
         echo "set State:/Network/Service/OpenVPN/IPv4" >>/tmp/tmpscutil
    
         # PPP
         echo "d.init" >>/tmp/tmpscutil
         echo "d.add Status 8" >>/tmp/tmpscutil
         echo "d.add InterfaceName ppp0" >>/tmp/tmpscutil
         echo "set Setup:/Network/Service/OpenVPN/PPP" >>/tmp/tmpscutil
         echo "set State:/Network/Service/OpenVPN/PPP" >>/tmp/tmpscutil
         
         # Interface
         echo "d.init" >>/tmp/tmpscutil
         echo "d.add Type PPP" >>/tmp/tmpscutil
         echo "d.add SubType OpenVPN" >>/tmp/tmpscutil
         echo "set Setup:/Network/Service/OpenVPN/Interface" >>/tmp/tmpscutil
     
         cat /tmp/tmpscutil | /usr/sbin/scutil
    
         # Add infos about connection to be displayed in GUI
         /Applications/GuizmOVPN.app/tools writeprefs InfosIPAddress "$ifconfig_local"
         if [ "$ifconfig_netmask" == "" ] ; then
              /Applications/GuizmOVPN.app/tools writeprefs InfosSubnetMask "255.255.255.255"
       /Applications/GuizmOVPN.app/tools writeprefs InfosGateway "$ifconfig_remote"
         else
              /Applications/GuizmOVPN.app/tools writeprefs InfosSubnetMask "$ifconfig_netmask"
       /Applications/GuizmOVPN.app/tools writeprefs InfosGateway "$InfosGateway"
         fi
         /Applications/GuizmOVPN.app/tools writeprefs InfosTrafficRedirected "$traffic_is_redirected"
    
         # Start the APSd
         #/bin/launchctl load /System/Library/LaunchDaemons/com.apple.apsd.plist
       ;;
    
       down)
         # Remove the connection infos
         /Applications/GuizmOVPN.app/tools writeprefs InfosIPAddress "" 
         /Applications/GuizmOVPN.app/tools writeprefs InfosSubnetMask ""
         /Applications/GuizmOVPN.app/tools writeprefs InfosGateway ""
         /Applications/GuizmOVPN.app/tools writeprefs InfosTrafficRedirected ""
    
          # Check if we have a stored PSID. If not, take the actual one
          OLD_PSID=`echo "show State:/Network/Service/OpenVPN/PSID" | /usr/sbin/scutil | grep PSID | awk '{print $3}'`
          if [ "$OLD_PSID" == "" ] ; then
              OLD_PSID=$PSID
          fi
     
          # If we have saved DNS, restore them
          if [ "$OLD_PSID" != "" ] && [ "`KeyExists State:/Network/Global/DNSold`" == "YES" ] ; then
              echo "Restoring DNS to $OLD_PSID";
              CopyKey Setup:/Network/Service/$OLD_PSID/DNSold Setup:/Network/Service/$OLD_PSID/DNS ;
              CopyKey State:/Network/Service/$OLD_PSID/DNSold State:/Network/Service/$OLD_PSID/DNS ;
              CopyKey State:/Network/Global/DNSold State:/Network/Global/DNS ;
    
       echo "remove Setup:/Network/Service/$OLD_PSID/DNSold" | /usr/sbin/scutil >/dev/null 2>&1 ;
       echo "remove State:/Network/Service/$OLD_PSID/DNSold" | /usr/sbin/scutil >/dev/null 2>&1 ;
       echo "remove State:/Network/Global/DNSold" | /usr/sbin/scutil >/dev/null 2>&1 ;
          fi
    
          # Check if we need to remove the route we added for the DNS
          if [ "$traffic_is_redirected" == "1" ] && [ "$route_net_gateway" != "`GetActualDNS $OLD_PSID`" ] && [ "$route_net_gateway" != "" ] && [ "`GetActualDNS $OLD_PSID`" != "" ] ; then
        echo "Removing route to `GetActualDNS $OLD_PSID` with gateway $route_net_gateway" 
       /sbin/route delete -net `GetActualDNS $OLD_PSID` $route_net_gateway 255.255.255.255 
          fi
    
          # Remove informations about the connection
          echo "remove State:/Network/Service/OpenVPN/DNS" | /usr/sbin/scutil >/dev/null 2>&1
          echo "remove State:/Network/Service/OpenVPN/IPv4" | /usr/sbin/scutil >/dev/null 2>&1
          echo "remove State:/Network/Service/OpenVPN/PPP" | /usr/sbin/scutil >/dev/null 2>&1
          echo "remove State:/Network/Service/OpenVPN/PSID" | /usr/sbin/scutil >/dev/null 2>&1
    
          echo "remove Setup:/Network/Service/OpenVPN/IPv4" | /usr/sbin/scutil >/dev/null 2>&1
          echo "remove Setup:/Network/Service/OpenVPN/PPP" | /usr/sbin/scutil >/dev/null 2>&1
          echo "remove Setup:/Network/Service/OpenVPN/Interface" | /usr/sbin/scutil >/dev/null 2>&1
       ;;
       *) echo "$0: invalid script_type $script_type" && exit 1 ;;
    esac
    
    #####
    
  5. 启动脚本
    我们来写一个启动脚本吧,connect.sh
    #!/bin/sh

    sudo -v
    sudo usbmuxd
    iproxy 2222 22 &
    sudo -n openvpn --config server.ovpn &
    sleep 1
    exec ssh root@localhost -p 2222 -R 1194:localhost:1194 /var/root/iptether/ovpncon.sh
    然后是 iPhone 端的启动脚本 ovpncon.sh
    #!/bin/sh

    (cd /var/root/iptether; /Applications/GuizmOVPN.app/openvpn --config client.ovpn --script-security 2) &
    还有 DNS 修复脚本,一般情况下是不需要的,但如果 DNS 不工作,就试试看 iPhone 端执行 ovpndns.sh
    #!/bin/sh

    /usr/sbin/scutil << EOF
    open
    d.init
    d.add ServerAddresses * 8.8.8.8 8.8.4.4
    set State:/Network/Service/OpenVPN/DNS
    quit
    EOF
  6. 配置 NAT 转发
    echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
    但是这会在关机后失效。如果希望下次开机自动开启转发,再修改 /etc/sysctl.conf
    net.ipv4.ip_forward = 1
    然后配置防火墙:
    sudo iptables -t nat -A POSTROUTING -j MASQUERADE
    同样,这个也是关机后丢失,如果想要保留,再修改 /etc/iptables/iptables.rules 并且开启系统的 iptables 服务(可能会因发行版而不同):
    *nat
    -A POSTROUTING -j MASQUERADE
    COMMIT
  7. Copy & Run!
    现在,把一大堆 OpenVPN 的配置文件和证书文件用 SCP 拷贝到 iPhone 的 /var/root/iptether 里面,用户名 root,密码 alpine(如果你没有修改,i-FunBox 之类的软件也没有替你修改的话)。
    还有,所有 .sh 结尾的文件都要给 0755 权限。
    然后,在电脑上 ./connect.sh,会自动调用刚刚拷贝过去的 ovpncon.sh 然后连接上网。Enjoy then!
    可能会有一些小 Bug,比如 iCloud 无线同步时好时坏(亲测 iOS 5.0.1 工作,iOS 6.1 不工作),或者是推送(亲测 iOS 4.2.1 不工作,iOS 5.0.1 工作,iOS 6.1 工作),又或者是 iMessage(亲测 iOS 5.0.1 不工作,iOS 6.1 工作)。