iOS - NetworkExtension 建立隧道(OpenVpn)

注意:由于简大叔对XXX关键字过敏,所以本文均用XXX代替V皮N。
需要实现Personal-XXX功能是苹果开发者账号才有权限开启,所以第一步先去开发者中心创建证书,并添加权限(此步骤省略,自己百度)

本文章针对的是OpenXXX !!!

我们将使用OpenXXXAdapter,使用Cocoapods进行安装

pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.4.0'

Carthage安装

github "ss-abramchuk/OpenVPNAdapter"

多target时,Cocoapods的格式如下:

platform :ios, '10.0'
target 'OpenSSLOnce' do
use_frameworks!
pod 'AFNetworking','~> 4.0'
pod 'MJRefresh'
pod 'SVProgressHUD'
post_install do |installer_representation|
installer_representation.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'NO'
end
end
end
end
target 'TargetTunnel' do
use_frameworks!
pod 'OpenVPNAdapter', :git => 'https://github.com/ss-abramchuk/OpenVPNAdapter.git', :tag => '0.4.0'
end

1、创建Target


创建Target.png

选择Network.png

2、创建完成后需要将主项目和子项目的Bundle Identifier进行替换,这里填写的是开发者中心创建好的Bundle Identifier,填写完成后父子项目添加相应的Capability,如下图


添加Capability.png

代码实现 - OC 版

  • 首先是子项目,也就是新创建的Target

PacketTunnelProvider.h

//
//  PacketTunnelProvider.h
//  TargetTunnel
//
//
//

@import NetworkExtension;
@import OpenVPNAdapter;

NS_ASSUME_NONNULL_BEGIN

@interface PacketTunnelProvider : NEPacketTunnelProvider

@property(nonatomic,strong) OpenVPNAdapter *vpnAdapter;

@property(nonatomic,strong) OpenVPNReachability *openVpnReach;

typedef void(^StartHandler)(NSError * _Nullable);
typedef void(^StopHandler)(void);

@property(nonatomic,copy) StartHandler __nullable startHandler;
@property(nonatomic,copy) StopHandler __nullable stopHandler;

@end

NS_ASSUME_NONNULL_END

PacketTunnelProvider.m

//
//  PacketTunnelProvider.m
//  TargetTunnel
//

#import "PacketTunnelProvider.h"
#include "NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h"

@interface PacketTunnelProvider ()<OpenVPNAdapterDelegate>
// 这个先放下,一会讲,主要用到和父项目进行通信
@property (strong,nonatomic) NSUserDefaults *userDefaults;

@end

@implementation PacketTunnelProvider

// 懒加载
-(OpenVPNAdapter*)vpnAdapter{
    
    if(!_vpnAdapter){
        
        _vpnAdapter = [[OpenVPNAdapter alloc] init];
        
        _vpnAdapter.delegate = self;
    }
    
    return _vpnAdapter;
}


-(OpenVPNReachability*)openVpnReach{
    
    if(!_openVpnReach){
        
        _openVpnReach = [[OpenVPNReachability alloc] init];
    }
    
    return _openVpnReach;
}

-(void)startTunnelWithOptions:(NSDictionary<NSString *,NSObject *> *)options completionHandler:(void (^)(NSError * _Nullable))completionHandler
{
    NETunnelProviderProtocol *proto =  (NETunnelProviderProtocol*)self.protocolConfiguration;
    
    if(!proto){
        
        return;
    }
    
    NSDictionary<NSString *,id> *provider = proto.providerConfiguration;
        
    NSData * fileContent = provider[@"ovpn"];
    
//    NSString * str1  = [[NSString alloc] initWithData:fileContent encoding:NSUTF8StringEncoding];


    OpenVPNConfiguration *openVpnConfiguration = [[OpenVPNConfiguration alloc] init];
    
    openVpnConfiguration.keyDirection = 1;
    
    openVpnConfiguration.fileContent = fileContent;
    // If true, don't send client cert/key to peer.
    openVpnConfiguration.disableClientCert = NO;
    // 用户名和密码进行认证
//    openVpnConfiguration.settings = @{@"username":@"",@"password":@""};
//    如果要在暂停或重新连接期间保持TUN接口处于活动状态,请取消对此行的注释
//    openVpnConfiguration.tunPersist = YES;
    
    NSError *error;

    OpenVPNProperties *evaluation = [self.vpnAdapter applyConfiguration:openVpnConfiguration error:&error];

    if(error){

        completionHandler(error);

        return;
    }
    // 配置用户名和密码
    if (!evaluation.autologin)
    {
        OpenVPNCredentials *tials = [[OpenVPNCredentials alloc]init];

        tials.username = [NSString stringWithFormat:@"%@",[options objectForKey:@"username"]];

        tials.password = [NSString stringWithFormat:@"%@",[options objectForKey:@"password"]];

        [self.vpnAdapter provideCredentials:tials error:&error];

        if(error){

            completionHandler(error);
            return;
        }
    }
    
    [self.openVpnReach startTrackingWithCallback:^(OpenVPNReachabilityStatus status) {
        
        if(status==OpenVPNReachabilityStatusReachableViaWiFi){
        
            [self.vpnAdapter reconnectAfterTimeInterval:5];
        }
    }];
    
    //建立连接并等待。关联事件
    self.startHandler = completionHandler;


    [self.vpnAdapter connect];
    
}


-(void)stopTunnelWithReason:(NEProviderStopReason)reason completionHandler:(void (^)(void))completionHandler
{
    self.stopHandler = completionHandler;

    if ([self.openVpnReach isTracking]) {
        // vpn被主动关闭
        [self.openVpnReach stopTracking];
    }
    
    [self.vpnAdapter disconnect];
}



- (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter configureTunnelWithNetworkSettings:(nullable NEPacketTunnelNetworkSettings *)networkSettings completionHandler:(nonnull void (^)(NSError * _Nullable))completionHandler {
    
    
    __weak __typeof(self) weak_self = self;
    
    [self setTunnelNetworkSettings:networkSettings completionHandler:^(NSError * _Nullable error) {
       
        if(!error){

            completionHandler(weak_self.packetFlow);
        }
    }];
}

- (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter handleError:(nonnull NSError *)error {

    BOOL isOpen = (BOOL)[error userInfo][OpenVPNAdapterErrorFatalKey];
    
    NSLog(@"isOpen = %d ",isOpen);

    if(isOpen){
    
        if (self.openVpnReach.isTracking) {
        
            [self.openVpnReach stopTracking];
        }
        
        if (error)
        {
            self.startHandler(error);
        }
                
        self.startHandler = nil;
    }
}

- (void)openVPNAdapter:(nonnull OpenVPNAdapter *)openVPNAdapter handleEvent:(OpenVPNAdapterEvent)event message:(nullable NSString *)message {
    
    switch (event) {
        case OpenVPNAdapterEventConnected:
        {
            if(self.reasserting){
                
                self.reasserting = false;
            }
            
            self.startHandler(nil);
            
            self.startHandler = nil;
        }
            break;
        case OpenVPNAdapterEventDisconnected:
        {
            if (self.openVpnReach.isTracking) {
                
                [self.openVpnReach stopTracking];
            }
            
            self.stopHandler();
            
            self.stopHandler = nil;
        }
            break;
        case OpenVPNAdapterEventReconnecting:
            self.reasserting = true;
            break;
        default:
            break;
    }
}

@end

NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h

//
//  NEPacketTunnelFlow+NEPacketTunnelFlow_Extension.h
//  PacketTunnel
//
#import <NetworkExtension/NetworkExtension.h>

@interface NEPacketTunnelFlow ()<OpenVPNAdapterPacketFlow>

@end
  • 下一步是父项目,分为初始化,建立连接,断开连接,监控状态
  1. 初始化,将XXX的配置信息进行保存,这里传的data,大概是如下格式:

client
dev tun
proto tcp或者udp
remote ip地址 端口
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
auth SHA512
cipher AES-256-CBC
ignore-unknown-option block-outside-dns
block-outside-dns
verb 3
<ca>
-----BEGIN CERTIFICATE-----
密钥
-----END CERTIFICATE-----
</ca>
<cert>
密钥
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
密钥
-----END PRIVATE KEY-----
</key>
<tls-crypt>
-----BEGIN OpenVPN Static key V1-----
密钥
-----END OpenVPN Static key V1-----
</tls-crypt>

保存vpn相关的数据

///  保存vpn相关的数据
/// @param data 数据
-(void)saveVpn:(NSData *)data
{
    //加载与调用应用程序关联的所有应用程序代理配置,这些配置以前已保存到网络扩展首选项中。
    [NETunnelProviderManager loadAllFromPreferencesWithCompletionHandler:^(NSArray<NETunnelProviderManager *> * _Nullable managers, NSError * _Nullable error) {
        if (error) {
            SSLog(@"Load Error: %@", error.description);
        }

        NETunnelProviderManager *manager;
        if (managers.count > 0) {
            manager = managers[0];
        }else {
            manager = [[NETunnelProviderManager alloc] init];
            manager.protocolConfiguration = [[NETunnelProviderProtocol alloc] init];
        }
        
        NETunnelProviderProtocol *tunel = [[NETunnelProviderProtocol alloc]init];
        // 获取文件内容
        tunel.providerConfiguration = @{@"ovpn": data};
        // 项目的Identifier
        tunel.providerBundleIdentifier = @"这里是子项目的BundleIdentifier";
        // serverAddress:即在手机设置的vpn中显示的vpn地址(服务器显示)
        tunel.serverAddress = @"openXXX";
//        tunel.username = @"username";
//        tunel.identityDataPassword = @"password";
        // 设备进入睡眠,vpn断开连接
        tunel.disconnectOnSleep = YES;
        // 是否可以编辑
        [manager setEnabled:YES];
        // 协议配置
        [manager setProtocolConfiguration:tunel];
        // 包含vpn描述的字符串(类型显示)
        manager.localizedDescription = @"openXXX";
        // 保存信息
        SSLWeakSelf(self);
        [manager saveToPreferencesWithCompletionHandler:^(NSError *error) {
            
            if(error) {
                
                SSLog(@"Save error: %@", error);
                
            }else {
                
                weakself.providerManagers = manager;
                                
                SSLog(@"add success");
                //加载与调用应用程序关联的所有应用程序代理配置,这些配置以前已保存到网络扩展首选项中。
                [manager loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
                    SSLog(@"loadFromPreferences!");

                }];
            }
        }];
 
    }];
}
  1. 建立隧道,开始连接
-(void)connect
{
    // 连接
    [self.providerManagers loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
        if(!error){
            NSError *error = nil;
            [self.providerManagers.connection startVPNTunnelWithOptions:nil andReturnError:&error];
            if(error) {
                SSLog(@"Start error: %@", error.localizedDescription);
            }else{
                SSLog(@"Connection established!");
            }
        }
    }];
}
  1. 断开连接
-(void)disconnectAction
{
    // 断开连接
    [self.providerManagers loadFromPreferencesWithCompletionHandler:^(NSError * _Nullable error) {
       
        [self.providerManagers.connection stopVPNTunnel];
    }];

}

4.监控XXX的状态

// 添加通知 - 连接信息改变时进行通知
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onVpnStateChange:) name:NEVPNStatusDidChangeNotification object:nil];

// 通知的方法
-(void)onVpnStateChange:(NSNotification *)Notification {
        
    NEVPNStatus status = self.providerManagers.connection.status;

    switch (status) {
        case NEVPNStatusInvalid:
        {
            SSLog(@"连接无效");

        }
            break;
        case NEVPNStatusDisconnected:
        {
            SSLog(@"未连接");
        }
            break;
        case NEVPNStatusConnecting:
        {
            SSLog(@"正在连接");
        }
            break;
        case NEVPNStatusConnected:
        {
            SSLog(@"已连接");
        }
           
            break;
        case NEVPNStatusDisconnecting:
        {
            SSLog(@"断开连接中...");
        }
           
            break;
        case NEVPNStatusReasserting:
        {
            SSLog(@"重新连接...");
        }
            break;
        default:
            break;
    }
}

下面说一下父子项目之间怎么进行通信,其实最基本的方法就是两个项目读取本地保存的文件,需要在开发者中心添加app groups,如下图


添加groups

添加通信,这里只是举了一个例子,也可以自行百度(多Target之间goup通信)

#pragma mark - NSUserDefaults,进行通信

// 获取
- (void)getRewardTimeFromMain{

    self.userDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"这里填对应的bundle Indentifier"];

    NSString *timeStr = [self.userDefaults objectForKey:@"key"];
}

// 保存
- (void)rewardTimeToMain:(NSInteger)timeNum{

    [self.userDefaults setObject:@"value" forKey:@"key"];

    [self.userDefaults synchronize];
}

代码实现 - Swift 版

父项目写了一个类,管理vpn的创建等步骤,代码如下:

VPNManager.swift
//
//  VPNManager.swift
//  VPNClient
//
//  Created by wl on 2021/3/15.
//

import Foundation
import NetworkExtension

class VPNManager {
    
    static let shared = VPNManager()
    
    var manager: NETunnelProviderManager?
    
    func connect() {
        guard self.manager != nil else {
            return
        }
        self.loadPreferences()
    }
    
    func disconnect() {
        self.manager?.connection.stopVPNTunnel()
    }
    
    //加载已保存的NETunnelProvider configurations
    func loadManager() {
        NETunnelProviderManager.loadAllFromPreferences { (managers, error) in
            guard error == nil else {
                return
            }
            if let manager = managers?.first {
                self.manager = manager
            } else {
                //新建
                self.manager = NETunnelProviderManager()
                self.manager?.localizedDescription = "myVPN"
            }
            print("VPNManager 初始化完成")
        }
    }
    
    //加载当前vpn配置
    func loadPreferences() {
        guard let manager = self.manager else {
            return
        }
        
        self.manager?.loadFromPreferences { (error) in
            guard error == nil else {
                return
            }
            
            // 如果没有对应的配置,我们需要新建配置
            if manager.protocolConfiguration == nil {
                manager.protocolConfiguration = self.newConfiguration()
            }
            
            // 设置完isEnabled需要保存配置,启动当前配置
            manager.isEnabled = true
            manager.saveToPreferences { (error) in
                guard error == nil else {
                    // 用户拒绝保存等情况,清空配置
                    manager.protocolConfiguration = nil
                    return
                }
                // 保存完成后我们需要重新加载配置,进行连接,
                //https://stackoverflow.com/questions/47550706/error-domain-nevpnerrordomain-code-1-null-while-connecting-vpn-server
                self.loadPreferencesAndStartTunnel()
            }
            
        }
    }
    
    func loadPreferencesAndStartTunnel()  {
        self.manager?.loadFromPreferences(completionHandler: { (error) in
            guard error == nil else {
                return
            }
            self.startTunnel()
        })
    }
    
    private func startTunnel() {
        do {
            try self.manager?.connection.startVPNTunnel()
        } catch  {
            print(error)
        }
    }
    

    
    func newConfiguration() -> NETunnelProviderProtocol {
        //加载ovpn文件
        guard
            let configurationFileURL = Bundle.main.url(forResource: "vpnclient", withExtension: "ovpn"),
            let configurationFileContent = try? Data(contentsOf: configurationFileURL)
        else {
            fatalError()
        }
        
        let tunnelProtocol = NETunnelProviderProtocol()
        tunnelProtocol.serverAddress = ""
        //指定network extension 确保bundleIdentifier和network extension的id一致
        tunnelProtocol.providerBundleIdentifier = "com.starpavilionlimited.freeouterspace.TargetTunnel"
        tunnelProtocol.providerConfiguration = ["ovpn": configurationFileContent]
        
        return tunnelProtocol
    }
    private init(){
        
    }
}

子项目则是建立隧道用的,代码如下:

//
//  PacketTunnelProvider.swift
//  vpn-tunnel
//

import NetworkExtension
import UIKit
import OpenVPNAdapter

extension NEPacketTunnelFlow: OpenVPNAdapterPacketFlow {}

class PacketTunnelProvider: NEPacketTunnelProvider {
    
    lazy var vpnAdapter: OpenVPNAdapter = {
           let adapter = OpenVPNAdapter()
           adapter.delegate = self

           return adapter
       }()

       let vpnReachability = OpenVPNReachability()

       var startHandler: ((Error?) -> Void)?
       var stopHandler: (() -> Void)?

    override func startTunnel(options: [String : NSObject]?, completionHandler: @escaping (Error?) -> Void) {
        guard
            let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol,
            let providerConfiguration = protocolConfiguration.providerConfiguration
        else {
            fatalError()
        }

        guard let ovpnFileContent: Data = providerConfiguration["ovpn"] as? Data else {
            fatalError()
        }

        let configuration = OpenVPNConfiguration()
        configuration.fileContent = ovpnFileContent

        // Uncomment this line if you want to keep TUN interface active during pauses or reconnections
        // configuration.tunPersist = true

        do {
            try vpnAdapter.apply(configuration: configuration)
        } catch {
            completionHandler(error)
            return
        }

        // Checking reachability. In some cases after switching from cellular to
        // WiFi the adapter still uses cellular data. Changing reachability forces
        // reconnection so the adapter will use actual connection.
        vpnReachability.startTracking { [weak self] status in
            guard status == .reachableViaWiFi else { return }
             self?.vpnAdapter.reconnect(afterTimeInterval: 5)
        }

        // Establish connection and wait for .connected event
        startHandler = completionHandler
//     cocoapos 倒入0.8版本就需要换方法了
//           vpnAdapter.connect(using: packetFlow)
        vpnAdapter.connect();
    }
    
    override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) {
        stopHandler = completionHandler

        if vpnReachability.isTracking {
            vpnReachability.stopTracking()
        }

        vpnAdapter.disconnect()
    }
    
}


extension PacketTunnelProvider: OpenVPNAdapterDelegate {
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: @escaping (OpenVPNAdapterPacketFlow?) -> Void) {
     
        networkSettings?.dnsSettings?.matchDomains = [""]
        
        setTunnelNetworkSettings(networkSettings) { error in
            
            completionHandler(self.packetFlow);
        }
        
    }
    
    

    

    // OpenVPNAdapter calls this delegate method to configure a VPN tunnel.
    // `completionHandler` callback requires an object conforming to `OpenVPNAdapterPacketFlow`
    // protocol if the tunnel is configured without errors. Otherwise send nil.
    // `OpenVPNAdapterPacketFlow` method signatures are similar to `NEPacketTunnelFlow` so
    // you can just extend that class to adopt `OpenVPNAdapterPacketFlow` protocol and
    // send `self.packetFlow` to `completionHandler` callback.
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, configureTunnelWithNetworkSettings networkSettings: NEPacketTunnelNetworkSettings?, completionHandler: @escaping (Error?) -> Void) {
        // In order to direct all DNS queries first to the VPN DNS servers before the primary DNS servers
        // send empty string to NEDNSSettings.matchDomains
        networkSettings?.dnsSettings?.matchDomains = [""]

        // Set the network settings for the current tunneling session.
        setTunnelNetworkSettings(networkSettings, completionHandler: completionHandler)
    }

    // Process events returned by the OpenVPN library
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleEvent event: OpenVPNAdapterEvent, message: String?) {
        switch event {
        case .connected:
            if reasserting {
                reasserting = false
            }

            guard let startHandler = startHandler else { return }

            startHandler(nil)
            self.startHandler = nil

        case .disconnected:
            guard let stopHandler = stopHandler else { return }

            if vpnReachability.isTracking {
                vpnReachability.stopTracking()
            }

            stopHandler()
            self.stopHandler = nil

        case .reconnecting:
            reasserting = true

        default:
            break
        }
    }

    // Handle errors thrown by the OpenVPN library
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleError error: Error) {
        // Handle only fatal errors
        guard let fatal = (error as NSError).userInfo[OpenVPNAdapterErrorFatalKey] as? Bool, fatal == true else {
            return
        }

        if vpnReachability.isTracking {
            vpnReachability.stopTracking()
        }

        if let startHandler = startHandler {
            startHandler(error)
            self.startHandler = nil
        } else {
            cancelTunnelWithError(error)
        }
    }

    // Use this method to process any log message returned by OpenVPN library.
    func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {
        // Handle log messages
    }
}

控制器视图只有两个按钮,对应着下面代码中的connect和dissconnect,直接上代码:

//
//  SwiftViewController.swift
//

import UIKit
import NetworkExtension

class SwiftViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        VPNManager.shared.loadManager()
        
        NotificationCenter.default.addObserver(self, selector: #selector(statusChange), name: .NEVPNStatusDidChange, object: nil)

    }
    
    
    @objc func statusChange() {
        guard let manager = VPNManager.shared.manager else {
            return
        }
        switch manager.connection.status {
            case .connected:
            print("已连接")
        case .connecting:
            print("正在连接")
        case .disconnected:
            print("未连接")
        case .disconnecting:
            print("正在断开连接")
        default:
            print("其他状态")

        }
    }
    


    @IBAction func dissconnect(_ sender: UIButton) {
     
        VPNManager.shared.disconnect()
    }
    
    
    
    @IBAction func connect(_ sender: UIButton) {
        
        VPNManager.shared.connect()
    }
}

附上demo地址,有需要可以下载。
注意!!!需要自己在开发者中心申请Bundle Identifier,进行替换,项目中有OC和Swift的,在运行时先删除对应的,项目结构如下:

项目结构

到此基本就完成了,这里提供一些参考连接以供使用,都是干货

参考连接1
参考连接2
参考连接3
参考连接4
参考连接5

我自己使用的是OC,但是在子Target中,用了Swift。具体为什么用这个,打个哑谜,你们自己试一试就知道了。

哦了,就这么多,有问题请指出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容